Search code examples
iosswiftswiftuiswiftui-navigationlinkswiftui-navigationstack

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


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]!
}

Solution

  • 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