3

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]!
}
  • Does this answer your question? [Only root-level navigation destinations are effective for a navigation stack with a homogeneous path](https://stackoverflow.com/questions/74362455/only-root-level-navigation-destinations-are-effective-for-a-navigation-stack-wit) – lorem ipsum Jul 06 '23 at 15:47
  • @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. – IamActuallyIronMan Jul 06 '23 at 18:14
  • You only need 1 you can include the id as an argument in the case for the enum – lorem ipsum Jul 06 '23 at 20:26
  • @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? – IamActuallyIronMan Jul 06 '23 at 20:47
  • That means there is another navigation destination. Check your code carefully. Another might be something that you are initializing twice – lorem ipsum Jul 10 '23 at 01:02

3 Answers3

3

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

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
2

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.

  • 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](https://stackoverflow.com/a/76657501/5623035) and let me know your opinion. – Mojtaba Hosseini Jul 12 '23 at 12:19
-2

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)
        }
    }
}
Sreeram Nair
  • 2,369
  • 12
  • 27
  • @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 console – IamActuallyIronMan Jul 09 '23 at 15:56
  • 3
    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](//meta.stackoverflow.com/q/421831). 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! – NotTheDr01ds Jul 22 '23 at 22:30
  • 3
    **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. – NotTheDr01ds Jul 22 '23 at 22:30