0

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 NavigationLinks 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?

Erhannis
  • 4,256
  • 4
  • 34
  • 48

1 Answers1

0

It seems your uiPrint function causes this behaviour. I could reproduce the example and it works when you don't create these additional 'none' views to print values during View creation.

Working example project: https://github.com/ralfebert/SwiftUIPlayground/blob/master/SwiftUIPlayground/Views/LinkedStateChangeView.swift

I am not sure why the additional 'none' views mess with the state behavior.

Also, works both with ForEach(self.items.indicies) and ForEach(self.items, id: \.self).

Ralf Ebert
  • 3,556
  • 3
  • 29
  • 43
  • That's...weird. You're right. It's weird because the code I distilled this from didn't have the `uiPrint` lines. I've discovered that in the example, if I wrap the contents of the `ForEach`s in `VStack`s, it works again. Maybe because `ForEach` wants a single view. Back in my original code, it looks unrelated, but I had two nested `ForEach`, and if I remove the outer one (it wasn't strictly necessary) then things seem to work correctly. Thanks for the clue – Erhannis May 18 '20 at 14:49
  • Looks like I was originally falling prey to https://stackoverflow.com/q/60878310/513038 , caused by https://stackoverflow.com/a/60416515/513038 . I don't know why specifically nested `ForLoop`s trigger the problem, but adding `id:\.self` to the `ForEach`s fixed it. – Erhannis May 18 '20 at 15:47