Multiple navigation destinations aren't working as expected
olehpidhi opened this issue · comments
Description
We have a screen that can trigger a long-running operation, and this navigates the user to a loading screen. After the operation is finished, automatic navigation to the result screen is triggered.
Checklist
- I have determined whether this bug is also reproducible in a vanilla SwiftUI project.
- If possible, I've reproduced the issue using the
main
branch of this package. - This issue hasn't been addressed in an existing GitHub issue or discussion.
Expected behavior
After the operation is finished, the user is navigated to a result screen.
Actual behavior
The behavior is unstable, sometimes I get navigated to the result screen successfully, and sometimes I get popped back to the screen that triggers the operation.
Steps to reproduce
Here's a sample project. It uses both vanilla SwiftUI .navigationDestination
and .navigationDestination
from SwiftUI-Navigation. Changing .navigationDestination
to .fullscreenCover
for loading screen makes behavior even more weird when even operation triggering screen can be poped from navigation stack.
SwiftUI Navigation version information
1.0.0
Destination operating system
16.4
Xcode version information
Version 14.3.1 (14E300c)
Swift Compiler version information
xcrun swiftc --version
swift-driver version: 1.75.2 Apple Swift version 5.8.1 (swiftlang-5.8.0.124.5 clang-1403.0.22.11.100)
Target: arm64-apple-macosx13.0
Hi @olehpidhi, this is unfortunately just another bug in SwiftUI navigation and is reproducible in vanilla SwiftUI navigation.
First, note that although you seem to have found a work around with your ad-hoc isPresented
bindings here:
//With setter doing nothing navigation works as expected
.navigationDestination(isPresented: Binding<Bool>(get: {
viewModel.destination == .destination3
}, set: { isPresented in
print(isPresented)
}), destination: {
Rectangle().foregroundColor(.cyan)
})
.navigationDestination(isPresented: Binding<Bool>(get: {
viewModel.destination == .destinationLoading
}, set: { isPresented in
print(isPresented)
}), destination: {
Rectangle().foregroundColor(.brown)
})
…this only causes other problems. You must implement the set
in the binding, otherwise your data model will get out of sync with SwiftUI. And you can see this for yourself by running the demo, tapping "1", tapping "2", waiting for the blue screen to appear, and then tapping "Back". At that moment view 2's destination
should be nil
, but it's actually destination3
. So now SwiftUI is in an inconsistent state.
If you update the set
in the bindings to actually perform the correct logic so that SwiftUI does not get out of sync with your model:
.navigationDestination(isPresented: Binding<Bool>(get: {
viewModel.destination == .destination3
}, set: { isPresented in
viewModel.destination = isPresented ? .destination3 : nil // ⬅️
}), destination: {
Rectangle().foregroundColor(.cyan)
})
.navigationDestination(isPresented: Binding<Bool>(get: {
viewModel.destination == .destinationLoading
}, set: { isPresented in
viewModel.destination = isPresented ? .destinationLoading : nil // ⬅️
}), destination: {
Rectangle().foregroundColor(.brown)
})
…then you will see the exact same problem that you see with our navigationDestination(unwrapping:case:)
. And it's not that surprising because our method just calls down to navigationDestination(isPresented:)
under the hood. They really shouldn't differ in behavior.
The unfortunate reality is that navigationDestination(isPresented:)
is really buggy in iOS 16. Things got a little better in iOS 17, but there are still some bugs. Your example does run correctly in iOS 17, but only if you swap the order of the navigationDestination
modifiers so that destinationLoading
comes before destination3
:
.navigationDestination(unwrapping: $viewModel.destination, case: /ViewModel2.Destination.destinationLoading) { $model in
…
}
.navigationDestination(unwrapping: $viewModel.destination, case: /ViewModel2.Destination.destination3) { $model in
…
}
Don't ask me why, I have no idea. 😅
Since this is not an issue with the library I am going to convert it to a discussion.