Reputation: 5213
I have the following SwiftUI set up
In my main, I have something like this
// singleton
@StateObject var authStatusManager = Factory.shared.authStatusManager
var body: some Scene {
WindowGroup {
if authStatusManager.isSignedIn {
NavigationStack {
ProjectsView(projectsViewModel: projectViewModel)
}
} else {
LoginView(loginViewModel: loginViewModel)
}
}
}
The first time the user will see the LoginView
but after logging in, we save the user token in user defaults and subsequently take the user directly to the ProjectsView
every time the app launches.
When the user successfully logs in, we also take the user to the ProjectsView
and it is in this journey where the problem arises. Here is how the LoginView
is set up at a high level:
var body: some View {
ZStack {
// different login elements
}
.onChange(of: loginViewModel.apiCallState) { currentAPIState in
if currentAPIState == .success {
// This causes the Projects view to get loaded
Factory.shared.authStatusManager.isSignedIn = true
}
}
The projects view is a simple List with some rows and works normally, the high level code is something like this:
var body: some View {
ZStack {
VStack {
List {
ForEach(Array(projectsViewModel.projectRowViewModels.enumerated()),
id: \.0) { index, projectViewModel in
Section {
NavigationLink(destination: CurrentProjectView(currentProjectViewModel: projectsViewModel.selectedProjectViewModel(at: index))) {
ProjectRow(projectRowViewModel: projectViewModel)
}
}
}
}
.id(UUID())
}
}
}
Everything is fine up until this point regardless of if we come from the LoginView path or directly to this view if the user is already logged in.
The issue arises when we go to the CurrentProjectView
which is also a list
var body: some View {
content
}
private var content: some View {
List {
projectImageView
ForEach(currentProjectViewModel.sections) { section in
if section == .surveys {
getSurveySection()
}
}
}
private func getSurveySection() -> some View {
ForEach(currentProjectViewModel.surveys.enumeratedArray(),
id: \.element) { index, field in
if index == 0 {
// create a button
}
getNavigationLink(index: index, field: field)
}
}
// This function seems to be called in an infinite loop when I tap on the navigation link
@ViewBuilder
private func getNavigationLink(index: Int, field: String) -> some View {
AnyView(NavigationLink(field) {
SurveyView(surveyViewModel: currentProjectViewModel.getSurveyViewModel(selectedSurveyIndex: index))
})
}
So the last line of code goes into an infinite loop only if we come from this path:
App Launch -> Login Screen -> Projects List Screen -> Current Project Screen
Tapping on the navigation link freezes the app and we have an infinite loop
However, if we come to the same screen after the user has already logged in and skipping the Login screen, everything works fine, that is:
App Launch -> Projects List Screen -> Current Project Screen
Tapping the same navigation link functions and takes the user to the right destination
I feel it could have something to do with how I've set up the navigation stack, however I can't seem to figure out the issue.
Upvotes: 2
Views: 699
Reputation: 1
When I removed the
@Environment(.dismiss) var dismiss
from the view which contained
Navigation Link
and
Navigation Destination
it worked for me
Upvotes: 0
Reputation: 2100
I was having this issue and the problem ended up being related to accessing properties outside of what was passed into a .navigationDestination
block.
Here's a simple example that had the issue. Notice that string
is being accessed but it was not passed into the .navigationDestination
block:
struct MainView: View {
@State var path = NavigationPath()
@State var string: String = ""
enum Destination {
case detail
}
var body: some View {
NavigationStack(path: $path) {
Button(action: {
path.append(Destination.detail)
}, label: {
Text("Push Detail view")
})
.navigationDestination(for: Destination.self, destination: { _ in
DetailView(viewModel: DetailViewModel(string: string))
})
}
}
}
class DetailViewModel: ObservableObject {
var string: String
init(string: String) {
self.string = string
}
}
struct DetailView: View {
@StateObject var viewModel: DetailViewModel
init(viewModel: DetailViewModel) {
self._viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View {
Text(viewModel.string)
}
}
A good rule of thumb seems to be to only access data that was passed into the .navigationDestination
block. You can achieve that in this simple example by associating the string
value to the Destination
enum case. Here's the updated MainView
struct:
struct MainView: View {
@State var path = NavigationPath()
@State var string: String = ""
enum Destination: Hashable {
case detail(string: String)
}
var body: some View {
NavigationStack(path: $path) {
Button(action: {
path.append(Destination.detail(string: string))
}, label: {
Text("Push Detail view")
})
.navigationDestination(for: Destination.self, destination: { destination in
if case let .detail(passedInString) = destination {
DetailView(viewModel: DetailViewModel(string: passedInString))
}
})
}
}
}
Upvotes: 0