SwiftUI a navigationDestination was declared earlier on the stack when pushing new value to the NavigationStack
Asked Answered
H

3

5

I am trying to recreate the account followers flow seen in many social media apps in SwiftUI.

  1. You press a button on your profile to see a list of your followers
  2. You can click on any one of your followers to see their account
  3. You can press a button on their profile to see a list of their followers
  4. You can click on any one of their followers to see their account

Steps 3 and 4 can go on forever (another example below):

MyProfile -> Followers (my followers list) -> FollowerView -> Followers (their followers list) -> FollowerView -> Followers (their followers list) -> FollowerView and so on...

However with the implementation below when run, the XCode console prints:

A navigationDestination for “myApp.SomeProfile” was declared earlier on the stack. Only the destination declared closest to the root view of the stack will be used.

I have an understanding as to why this is yet am unsure how to fix this issue. I am also if the type used as the NavigationLink value is suitable since it is Int. Would it be better to replace it with a more custom type?

Any help would be greatly appreciated.

// Enum with custom options
enum ViewOptions: Hashable {
    case followers(Int)
    
    @ViewBuilder func view(_ path: Binding<NavigationPath>, id: Int) -> some View {
        FollowersList(path: path, id: id)
    }
}
// Root view
struct MyProfileView: View {
    @State private var path: NavigationPath = .init()
    
    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                Text(myProfile.username)
                Button("See followers") {
                    path.append(ViewOptions.followers(myProfile.id))
                }
                .navigationDestination(for: ViewOptions.self) { option in
                    option.view($path, id: myProfile.id)
                }
            }
        }
    }
}
struct FollowersList: View {
    @Binding var path: NavigationPath
    var id: Int
    
    var body: some View {
        List(getFollowers(for: id), id:\.id) { follower in
            NavigationLink(follower.username, value: follower)
        }
        .navigationDestination(for: SomeProfile.self) { profile in
            switch profile.isMe {
            case true:  Text("This is your profile")
            case false: SomeProfileView(path: $path, profile: profile)
            }
        }
    }
}
struct SomeProfileView: View {
    @Binding var path: NavigationPath
    
    var profile: SomeProfile
    var body: some View {
        VStack {
            Text(profile.username)
            Button("See followers") {
                path.append(ViewOptions.followers(profile.id))
            }
        }
    }
}

// ----- Types & functions -----

// Example type for my profile
struct MyProfile: Identifiable, Hashable {
    var id: Int
    var username: String
}
// Example type for profiles reached via navigation link
// (can be my profile but with reduced functionality e.g. no follow button)
struct SomeProfile: Identifiable, Hashable {
    var id: Int
    var username: String
    let isMe: Bool
}
// example myProfile (IRL would be stored in a database)
let myProfile = MyProfile(id: 0, username: "my profile")
// example users (IRL would be stored in a database)
let meVisited = SomeProfile(id: 0, username: "my profile reached from followers list", isMe: true)
let bob       = SomeProfile(id: 1, username: "Bob", isMe: false)
let alex      = SomeProfile(id: 2, username: "Alex", isMe: false)
// example user followers (IRL would be stored in a database)
let dict: [Int : [SomeProfile]] = [
    0 : [bob, alex],
    1 : [alex, meVisited],
    2 : [alex, meVisited],
]
// example function to get followers of a user (IRL would be a network request)
func getFollowers(for id: Int) -> [SomeProfile] {
    return dict[id]!
}
Heyerdahl answered 6/7, 2023 at 14:46 Comment(6)
Does this answer your question? Only root-level navigation destinations are effective for a navigation stack with a homogeneous pathColumbuscolumbyne
@loremipsum Partially, I have adopted the answer from that question into the code above yet I am still receiving the same error in the console. I have updated the code in the question with the changes I made.Heyerdahl
You only need 1 you can include the id as an argument in the case for the enumColumbuscolumbyne
@loremipsum I've tried that. It still prints the error in the console with the .navigationDestination removed from SomeProfileView so there is only one. Also, what do you mean by the id an argument in the case for the enum? Have I not already done that?Heyerdahl
That means there is another navigation destination. Check your code carefully. Another might be something that you are initializing twiceColumbuscolumbyne
Please make sure, you are not calling the .navigationDestination inside a loop.Complicated
S
8

You are repeatedly adding an identical .navigationDestination(...) modifier by showing views that contain it.

Move all navigationDestination log somewhere that is not repeating like (but not necessarily) in the NavigationStack level.

Like this:

NavigationStack(path: $path) {
    VStack {
        Text(myProfile.username)
        Button("See followers") {
            path.append(ViewOptions.followers(myProfile.id))
        }
        .navigationDestination(for: ViewOptions.self) { option in
            option.view($path, id: myProfile.id)
        }
        .navigationDestination(for: SomeProfile.self) { profile in
            switch profile.isMe {
            case true:  Text("This is your profile")
            case false: SomeProfileView(path: $path, profile: profile)
            }
        }
    }
}

So .navigationDestination(for: SomeProfile.self) will not be created again and again.

Don't forget to remove it from the FollowersList

Spirogyra answered 10/7, 2023 at 21:39 Comment(0)
H
3

I managed to solve the problem by creating an Options enum.

enum Options: Hashable {
    case destination(Int)
}

In MyProfileView, add .navigationDestination modifiers so they are outside of repeating views.

.navigationDestination(for: UserInfoPreview.self) { userInfoPreview in
    UserInfoView(id: userInfoPreview.id)
}
.navigationDestination(for: Options.self) { destination in
    switch destination {
    case .destination(let userID): FollowersList(id: userID)
    }
}

Updated NavigationLink in SomeProfileView.

NavigationLink("See followers", value: Options.destination(id))

Removed path binding from views too.

Heyerdahl answered 11/7, 2023 at 13:41 Comment(6)
Glad to overcome your issue by refactoring the code. If you want to know why this is yet am unsure how to fix this issue as you asked in the question, please see my answer and let me know your opinion.Spirogyra
Thanks! After searching and testing for days, this is the only viable method for large code bases with deep navigation. The same Object Type is used to launch different views deeper in the stack which brakes most of the .navigationDestination examples which use the Object Type as the parameter. Hopefully Apple makes this easier as this logic is very difficult to follow in deep navigation.Euphoria
Unfortunately after running this solution for a few days I noticed that the views are being recreated which prevents bindings.Euphoria
@JavierRefuerzo I believe I encountered the same issue. I removed the bindings which led to data inconsistencies. When going deeper in the navigation the data remains consistent as in my case I make an API call to load the users profile. The issue I had was as I went back to the surface, there were inconsistencies if the data in question had being changed deeper in the navigation.Heyerdahl
@SamuelDavies I also noticed this method also calls onAppear on in the calling View which cause other issues such as data reload. If it helps anyone else I ended up going with navigationDestination(isPresented:destination:) while not as intuitive this allows the calling View to define the transition instead a root view. navigation. See: developer.apple.com/documentation/swiftui/view/… Example NavigationLink isActive deprecated. How to convert to new api? - developer.apple.com/forums/thread/…Euphoria
@JavierRefuerzo In the end i used programmatic navigation to add each type to a NavigationPath. I am still yet to see any issues. As for the .onAppear, I created a ViewModifier which only calls .onAppear on the first appearance of the view.Heyerdahl
G
-5

NavigationLink destinations are being declared multiple times in the navigation stack, which is causing the error.

I have tried the below steps and modified your code. Hope it solves your issue.

1.  Replace the Int type in ViewOptions with SomeProfile to represent the follower profile directly instead of using an identifier.
2.  Update the NavigationLink in FollowersList to use the follower profile directly instead of its username.
3.  Modify the NavigationStack in MyProfileView to pass the id of the selected follower instead of myProfile.id.
4.  Remove the navigationDestination modifier from the Button in MyProfileView.

Here’s the updated code:

// Enum with custom options
enum ViewOptions: Hashable {
    case followers(SomeProfile)
    
    @ViewBuilder func view(_ path: Binding<NavigationPath>, profile: SomeProfile) -> some View {
        FollowersList(path: path, profile: profile)
    }
}

// Root view
struct MyProfileView: View {
    @State private var path: NavigationPath = .init()
    
    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                Text(myProfile.username)
                Button("See followers") {
                    path.append(ViewOptions.followers(myProfile))
                }
            }
        }
        .navigationDestination(for: ViewOptions.self) { option in
            option.view($path, profile: myProfile)
        }
    }
}

struct FollowersList: View {
    @Binding var path: NavigationPath
    var profile: SomeProfile
    
    var body: some View {
        List(getFollowers(for: profile.id), id: \.id) { follower in
            NavigationLink(destination: follower) {
                Text(follower.username)
            }
        }
    }
}

struct SomeProfileView: View {
    @Binding var path: NavigationPath
    
    var profile: SomeProfile
    
    var body: some View {
        VStack {
            Text(profile.username)
            Button("See followers") {
                path.append(ViewOptions.followers(profile))
            }
        }
        .navigationDestination(for: ViewOptions.self) { option in
            option.view($path, profile: profile)
        }
    }
}
Gloria answered 8/7, 2023 at 22:14 Comment(3)
@SreeremNair Thank you for taking the time to respond. However, the code you provided above still results in the error: ... was declared earlier on the stack being printed in the consoleHeyerdahl
Hi, Sreeram Nair. Most or all of your last 11 answers appear likely to be entirely or partially written by AI (e.g., ChatGPT). Many also have comments indicating that they are wrong in some respect. Please be aware that posting AI-generated content is not allowed here. If you used an AI tool to assist with any answer, I would encourage you to delete it. We do hope you'll stick around and continue to be a valuable part of our community by posting your own quality content. Thanks!Hitandmiss
Readers should review this answer carefully and critically, as AI-generated information often contains fundamental errors and misinformation. If you observe quality issues and/or have reason to believe that this answer was generated by AI, please leave feedback accordingly.Hitandmiss

© 2022 - 2024 — McMap. All rights reserved.