pointfreeco / swiftui-navigation

This package is now Swift Navigation:

Home Page:https://github.com/pointfreeco/swift-navigation

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.

NavigationTest.zip

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.