Why won't my checkbox UI update on the first click, but will update on every click after that? - kotlin

I have a Jetpack Compose for Desktop UI application that shows a popup with a list of enums, each of which has a checkbox to toggle them:
Sorry for the long code, this is as small a MCVE as I could make of it.
enum class MyEnum {
A, B, C
}
data class MyFilter(val enums: Collection<MyEnum>)
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
App()
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun App() {
var filter by remember { mutableStateOf(MyFilter(emptyList())) }
MaterialTheme {
var show by remember { mutableStateOf(false) }
if (show) {
val selected = remember { filter.enums.toMutableStateList() }
AlertDialog({ show = false }, text = {
Column {
MyEnum.values().forEach { e ->
Row {
val isSelected by remember { derivedStateOf { e in selected } }
Checkbox(isSelected, { if (isSelected) selected.remove(e) else selected.add(e) })
Text(e.name)
}
}
}
}, buttons = {
Button({
filter = MyFilter(selected.toList())
show = false
}) { Text("OK") }
})
}
Button({ show = true }) { Text("Open") }
}
}
The problem here is, the very first time after opening the dialog, a click on a checkbox will properly update the underlying selected list, but it won't update the UI. So the checkbox doesn't appear to change state. Every click after that will properly update the UI, including the changed state for the previous click.
This happens reliably, every single time the dialog is opened, but only on the first checkbox click.
Why does this happen, and how can I fix it?

Related

iOS16 - How to navigate through hidden links with an optional value?

I previously used an optional value to click a hidden link to navigate in my app. Something like this example in Hacking With Swift:
#State private var selection: String? = nil
var body: some View {
....
NavigationLink(destination: Text("View A"), tag: "A", selection: $selection) { EmptyView() }
NavigationLink(destination: Text("View B"), tag: "B", selection: $selection) { EmptyView() }
Button("Tap to show A") {
selection = "A"
}
Button("Tap to show B") {
selection = "B"
}
With iOS 16 this is deprecated. I am currently setting an optional value and when it's not nil I want a link to open. I can't figure out how to do it with the new NavigationLink/Value/Destination combination. Has anyone else figured out how to do it?
I created a new projects and switched ContentView to the following:
private enum Destinations: Hashable {
case empty
case general
case myQuestionView(String)
}
struct ContentView: View {
#State private var selection: Destinations?
#State private var mySelectedString: String?
var body: some View {
NavigationSplitView {
List(selection: $selection) {
NavigationLink(value: Destinations.general) {
Text("Example")
}
if mySelectedString != nil {
NavigationLink(value: Destinations.myQuestionView(mySelectedString!)) {
Text("String: \(mySelectedString ?? "no name")")
}
}
Button(action: {
mySelectedString = "A Name"
}, label: {
Text("Set the value")
})
}
} detail: {
NavigationStack {
switch selection ?? .empty {
case .empty: Text("Please select an option to continue.")
case .general: Text("Result of this option")
case .myQuestionView(let aString): Text("Hello \(aString)")
}
}
}
}
}
Here the Set the value button sets the selectedString which makes the link appear but I can't make it automatically navigate AND, ideally, it would never appear and would navigate when the value is set.
Okay... the answer is super simple and leaving the question and this answer up in case it helps someone else.
You don't need to have an invisible NavigationLink. You just need to set the selection to the new destination: selection = .myQuestionView("this name instead"). Or, if I was using the navigation path NavigationStack(path: $myPath) I'd just append it.
So in this specific example ContentView would now be:
private enum Destinations: Hashable {
case empty
case general
case myQuestionView(String)
}
struct ContentView: View {
#State private var selection: Destinations?
var body: some View {
NavigationSplitView {
List(selection: $selection) {
NavigationLink(value: Destinations.general) {
Text("Example")
}
Button(action: {
selection = .myQuestionView("this name instead")
}, label: {
Text("Set the value")
})
}
} detail: {
NavigationStack {
switch selection ?? .empty {
case .empty: Text("Please select an option to continue.")
case .general: Text("Result of this option")
case .myQuestionView(let aString): Text("Hello \(aString)")
}
}
}
}
}

How to change button property when starting a vuex-electron app?

I am working on a vuex-electron application.
There are several buttons on the main page, when clicked, a file is stored in a specific folder, and at the same time, the property of the button turns to [saved], when this [saved] button is clicked again, a message pops up, asking whether to overwrite or not the old file.
But the problem is : when the app is restarted, all of the buttons' property is initialized to [not saved], so, even though the same file has already been stored, when button is clicked, the old file is overwritten without any pop-ups asking whether to overwrite the existing file or not.
I want to change the feature as below:
when the app is restarted, check file existence first, then based on the result, change button property to [saved].
Is this possible?
If possible, where should I add the logic in.
I am a complete beginner on vue.js.
Have been looking up for a while, but did not find any useful information.
From the following thread, learned that state data is stored in a json file, does this mean I need to change the state? Currently, button properties are not saved in this json file.
Where is the state of a Electron-Vue application stored?
//code
v-for="(btn,i) in buttons" :key="i" #click="save(btn)"
computed: {
buttons() {
let r = this.$store.state.App.aData.filter(x=> {
if (this.aType==='abc') {
return x.video.toString() === '1'
} else {
return !x.video
}
}).map(x => {
let y = this.$copy(x)
y.saved = this.savedData.includes(x.index)
y.disabled = this.appState !== 'def'
return y
})
let index = this.aType==='abc'?998:999
r.push({
index,
a_name:'others',
e_number:this.aType==='abc'?'N':'999',
disabled: this.appState !== 'def',
saved: this.savedData.includes(index),
video:''
})
return r
},
},
methods:{
save(btn) {
if (btn.disabled) {
return
}
if (btn.saved) {
this.$buefy.dialog.confirm({
message: '??',
confirmText: 'overwrite?',
cancelText: 'cancel',
type: 'is-danger',
hasIcon: true,
onConfirm: () => {
if (this.aType==='xyz') {
this.saveXyz(btn)
}
if (this.aType==='abc') {
this.saveAbc(btn)
}
}
})
return
}
if (this.aType==='xyz') {
this.saveXyz(btn)
}
if (this.shootingType==='abc') {
this.saveAbc(btn)
}
},
saveXyz(data) {
if (this.xyzBuffer) {
//create a file and store to a folder
this.savedData.push(data.index)
let idx = this.$store.state.App.aData.findIndex(x=>x.index===data.index)
if (idx > -1) {
idx++
if (idx < this.$store.state.App.aData.length) {
this.selectedData = idx
this.aType = this.$store.state.App.aData[this.selectedData].video ? 'abc' : 'xyz'
this.scrollTo(this.selectedData)
}
}
this.cancel()
})
}
}
},
saveAbc(data) {
if (this.recording) {
return
}
//create a file and store to a folder
this.savedData.push(data.index)
let idx = this.$store.state.App.aData.findIndex(x=>x.index===data.index)
if (idx > -1) {
idx++
if (idx < this.$store.state.App.aData.length) {
this.selectedData = idx
this.aType = this.$store.state.App.aData[this.selectedData].video ? 'abc' : 'xyz'
this.scrollTo(this.selectedData)
}
}
this.cancel()
}
})
},
},

How to make a button a menu trigger in Jetpack Compose?

I tried adding an IconButton() and wanted to click the button to turn it on or off (while also clicking elsewhere to dismiss it)
But a funny qustion happend.
The trigger button is included in "Other Places", when I click the button to close the menu, this triggers onDismissRequest() first, and then triggers the button's onClick(), which makes me unable to close the menu (when clicked will instantly close and then open again)
Scaffold(
...
topBar = {
TopAppBar(
...
actions = {
var menuExpanded by remember { mutableStateOf(false) }
Box {
IconButton(onClick = { menuExpanded = !menuExpanded}) {
Icon(
painter = painterResource(id = R.drawable.menu),
contentDescription = "menu",
tint = white
)
}
DropdownMenu(
expanded = menuExpanded,
properties = PopupProperties(),
onDismissRequest = { menuExpanded = false }
) {
// items
}
}
}
)
}
) { ... }
I know I can set Modifier.offset() so that menu masks the button, but I don't want to do that.
What should I do?
This is what PopupProperties.focusable is for: true value prevents other views from getting tapped while the menu is open. By the way, this is the default value if you don't specify properties option.
DropdownMenu(
expanded = menuExpanded,
properties = PopupProperties(focusable = true),
onDismissRequest = { menuExpanded = false }
) {

Load more functionality using SwiftUI

i have used ScrollView with HStack, now i need to load more data when user reached scrolling at last.
var items: [Landmark]
i have used array of items which i am appeding in HStack using ForEach
ScrollView(showsHorizontalIndicator: false) {
HStack(alignment: .top, spacing: 0) {
ForEach(self.items) { landmark in
CategoryItem(landmark: landmark)
}
}
}
What is the best possible solution to manage load more in SwiftUI without using custom action like loadmore button.
It's better to use ForEach and List for this purpose
struct ContentView : View {
#State var textfieldText: String = "String "
private let chunkSize = 10
#State var range: Range<Int> = 0..<1
var body: some View {
List {
ForEach(range) { number in
Text("\(self.textfieldText) \(number)")
}
Button(action: loadMore) {
Text("Load more")
}
}
}
func loadMore() {
print("Load more...")
self.range = 0..<self.range.upperBound + self.chunkSize
}
}
In this example each time you press load more it increases range of State property. The same you can do for BindableObject.
If you want to do it automatically probably you should read about PullDownButton(I'm not sure if it works for PullUp)
UPD:
As an option you can download new items by using onAppear modifier on the last cell(it is a static button in this example)
var body: some View {
List {
ForEach(range) { number in
Text("\(self.textfieldText) \(number)")
}
Button(action: loadMore) {
Text("")
}
.onAppear {
DispatchQueue.global(qos: .background).asyncAfter(deadline: DispatchTime(uptimeNanoseconds: 10)) {
self.loadMore()
}
}
}
}
Keep in mind, that dispatch is necessary, because without it you will have an error saying "Updating table view while table view is updating). Possible you may using another async way to update the data
If you want to keep using List with Data instead of Range, you could implement the next script:
struct ContentView: View {
#State var items: [Landmark]
var body: some View {
List {
ForEach(self.items) { landmark in
CategoryItem(landmark: landmark)
.onAppear {
checkForMore(landmark)
}
}
}
}
func checkForMore(_ item: LandMark) {
guard let item = item else { return }
let thresholdIndex = items.index(items.endIndex, offsetBy: -5)
if items.firstIndex(where: { $0.id == item.id }) == thresholdIndex {
// function to request more data
getMoreLandMarks()
}
}
}
Probably you should work in a ViewModel and separate the logic from the UI.
Credits to Donny Wals: Complete example

Transition with keepScrollPosition and navigateBack

We are using Durandal for our SPA application and came to a, in my opinion, common use case. We have two pages: one page is a list of entities (with filters, sorting, virtual scroll) and another is detail preview of an entity. So, user is on list page and set a filter and a list of results comes out. After scrolling a little bit down user notice an entity which he/she would like to see details for. So clicking on a proper link user is navigated to details preview page.
After "work finished" on preview page user click back button (in app itself or browser) and he/she is back on the list page. However, default 'entrance' transition scroll the page to the top and not to the position on list where user pressed preview. So in order to 'read' list further user have to scroll down where he/she was before pressing preview.
So I started to create new transition which will for certain pages (like list-search pages) keep the scroll position and for other pages (like preview or edit pages) scroll to top on transition complete. And this was easy to do however, I was surprised when I noticed that there are strange behavior on preview pages when I hit navigateBack 'button'. My already long story short, after investigation I found out that windows.history.back is completing earlier then the transition is made and this cause that preview pages are scrolled automatically down to position of previous (list) page when back button is hit. This scrolling have a very unpleasant effect on UI not mentioning that it is 'total catastrophe' for my transition.
Any idea or suggestion what could I do in this case?
Here is the code of transition. It is just a working copy not finished yet as far as I have this problem.
define(['../system'], function (system) {
var fadeOutDuration = 100;
var scrollPositions = new Array();
var getScrollObjectFor = function (node) {
var elemObjs = scrollPositions.filter(function (ele) {
return ele.element === node;
});
if (elemObjs.length > 0)
return elemObjs[0];
else
return null;
};
var addScrollPositionFor = function (node) {
var elemObj = getScrollObjectFor(node);
if (elemObj) {
elemObj.scrollPosition = $(document).scrollTop();
}
else {
scrollPositions.push({element: node, scrollPosition: $(document).scrollTop()});
}
};
var scrollTransition = function (parent, newChild, settings) {
return system.defer(function (dfd) {
function endTransition() {
dfd.resolve();
}
function scrollIfNeeded() {
var elemObj = getScrollObjectFor(newChild);
if (elemObj)
{
$(document).scrollTop(elemObj.scrollPosition);
}
else {
$(document).scrollTop(0);
}
}
if (!newChild) {
if (settings.activeView) {
addScrollPositionFor(settings.activeView);
$(settings.activeView).fadeOut(fadeOutDuration, function () {
if (!settings.cacheViews) {
ko.virtualElements.emptyNode(parent);
}
endTransition();
});
} else {
if (!settings.cacheViews) {
ko.virtualElements.emptyNode(parent);
}
endTransition();
}
} else {
var $previousView = $(settings.activeView);
var duration = settings.duration || 500;
var fadeOnly = !!settings.fadeOnly;
function startTransition() {
if (settings.cacheViews) {
if (settings.composingNewView) {
ko.virtualElements.prepend(parent, newChild);
}
} else {
ko.virtualElements.emptyNode(parent);
ko.virtualElements.prepend(parent, newChild);
}
var startValues = {
marginLeft: fadeOnly ? '0' : '20px',
marginRight: fadeOnly ? '0' : '-20px',
opacity: 0,
display: 'block'
};
var endValues = {
marginRight: 0,
marginLeft: 0,
opacity: 1
};
$(newChild).css(startValues);
var animateOptions = {
duration: duration,
easing : 'swing',
complete: endTransition,
done: scrollIfNeeded
};
$(newChild).animate(endValues, animateOptions);
}
if ($previousView.length) {
addScrollPositionFor(settings.activeView);
$previousView.fadeOut(fadeOutDuration, startTransition);
} else {
startTransition();
}
}
}).promise();
};
return scrollTransition;
});
A simpler approach could be to store the scroll position when the module deactivates and restore the scroll on viewAttached.
You could store the positions in some global app variable:
app.scrollPositions = app.scrollPositions || {};
app.scrollPositions[system.getModuleId(this)] = theCurrentScrollPosition;