Swift 5.2
I have the following code. (It's been distilled from a much more complicated multi-page UI.) The core bit is that there are NavigationLink
s whose text reflects their contents, and when you click on them you can alter the contents. (I realize that in this simplified example, enabled
is class-wide, rather than specific to the link. This is a simplification. It still illustrates my problem.) Now, what I expect to happen is that when I click into the link ("disabled"), click the toggle, and then leave the nav link, the item should now say "enabled". It does not, but rather still says "disabled". Consider the UI:
struct ContentView: View {
@State var enabled: Bool = false
@State var items: [String] = ["item"]
var body: some View {
NavigationView {
Section {
uiPrint("CV main")
ForEach(self.items.indices) { idx in
uiPrint("CV item \(idx)")
NavigationLink(destination: Toggle(isOn: self.$enabled){Text("enable")}) {
uiPrint("CV link \(idx)")
if self.enabled {
Text("enabled")
} else {
Text("disabled")
}
}
}
}
}
}
}
public func uiPrint(_ str: String) -> AnyView? {
print(str)
return Optional<AnyView>.none
}
When I run this, I get logs (with my own actions written in in brackets) :
CV main
CV item 0
CV link 0
[CLICK LINK]
[CLICK TOGGLE]
2020-05-15 21:38:29.726757-0400 Test[39097:798734] invalid mode 'kCFRunLoopCommonModes' provided to CFRunLoopRunSpecific - break on _CFRunLoopError_RunCalledWithInvalidMode to debug. This message will only appear once per execution.
CV main
[CLICK BACK]
and the link still reads "disabled". Note that the logs indicate that while the main view is rerendered on toggle, the ForEach
is not.
Why is this, and how can I get the main view to fully update when the contents change (as changed by the contents themselves)?
I suspect it has something to do with the warning - breaking on the recommended symbol yields the following stack trace:
#0 0x0000000182b4d47c in _CFRunLoopError_RunCalledWithInvalidMode ()
#1 0x00000001035cf18c in _dispatch_client_callout ()
#2 0x00000001035d0bd8 in _dispatch_once_callout ()
#3 0x0000000182b4d6c4 in CFRunLoopRunSpecific ()
#4 0x0000000186741858 in __88-[UISwitchModernVisualElement _handleLongPressWithGestureLocationInBounds:gestureState:]_block_invoke ()
#5 0x0000000186d2bc10 in _runAfterCACommitDeferredBlocks ()
#6 0x0000000186d1b13c in _cleanUpAfterCAFlushAndRunDeferredBlocks ()
#7 0x0000000186d4c88c in _afterCACommitHandler ()
#8 0x0000000182b52c54 in __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ ()
#9 0x0000000182b4d8e4 in __CFRunLoopDoObservers ()
#10 0x0000000182b4dd84 in __CFRunLoopRun ()
#11 0x0000000182b4d660 in CFRunLoopRunSpecific ()
#12 0x000000018cf5e604 in GSEventRunModal ()
#13 0x0000000186d2215c in UIApplicationMain ()
#14 0x000000010290982c in main at REDACTED/AppDelegate.swift:13
#15 0x00000001829c91ec in start ()
which doesn't seem too helpful, aside from the fact that UISwitchModernVisualElement
(probably the toggle) is implicated.
Answer https://stackoverflow.com/a/59019554/513038 suggests that it's because the view is open and like, locked(?) when the state is changed, or something, but the proposed solution (make stuff reflecting changes not part of the chain of things triggering the change, I think) doesn't seem applicable, because each list item needs both its own display of state, as well as a link to a way to change it, and when a change occurs the list isn't even re-laid-out. How can I get it to behave as expected?