amodrono
amodrono

Reputation: 2040

How to have a NavigationLink inside another NavigationLink

My team and I are currently developing a Mastodon client in SwiftUI and I've got a simple StatusView where I display all the post data, which currently looks like this:

An example post

This view, known on my project as StatusView, has two NavigationLinks: The main one, that redirects the user to the post's thread, and one that redirects the user to the post's author's profile when the post's profile picture is tapped.

Until here, everything works fine. If you tap anywhere on the post that aren't the buttons (like, boost, and share) or the profile picture, it opens the thread. If you tap on the profile picture, it opens the author's profile.

enter image description here

But if you tap below the profile picture, the app crashes, only giving me the following error:

2020-08-23 21:32:39.929392-0400 Hyperspace[830:147862] WF: _WebFilterIsActive returning: NO
2020-08-23 21:32:40.117865-0400 Hyperspace[830:147862] [assertion] Error acquiring assertion: <Error Domain=RBSAssertionErrorDomain Code=2 "Specified target process does not exist" UserInfo={NSLocalizedFailureReason=Specified target process does not exist}>
2020-08-23 21:32:40.117994-0400 Hyperspace[830:147862] [ProcessSuspension] 0x11decfa80 - ProcessAssertion: Failed to acquire RBS Background assertion 'WebProcess Background Assertion' for process with PID 837, error: Error Domain=RBSAssertionErrorDomain Code=2 "Specified target process does not exist" UserInfo={NSLocalizedFailureReason=Specified target process does not exist}
2020-08-23 21:32:40.119401-0400 Hyperspace[830:147862] [assertion] Error acquiring assertion: <Error Domain=RBSAssertionErrorDomain Code=2 "Specified target process does not exist" UserInfo={NSLocalizedFailureReason=Specified target process does not exist}>
2020-08-23 21:32:40.119549-0400 Hyperspace[830:147862] [ProcessSuspension] 0x11decfac0 - ProcessAssertion: Failed to acquire RBS Suspended assertion 'WebProcess Suspended Assertion' for process with PID 837, error: Error Domain=RBSAssertionErrorDomain Code=2 "Specified target process does not exist" UserInfo={NSLocalizedFailureReason=Specified target process does not exist}
Fatal error: UIKitNavigationBridge: multiple active destinations: file SwiftUI, line 0
2020-08-23 21:32:40.292135-0400 Hyperspace[830:147862] Fatal error: UIKitNavigationBridge: multiple active destinations: file SwiftUI, line 0

I guess this is because when you tap there, both the thread's and the profile's navigation links are triggered, causing the app to crash because there are multiple active destinations, as it's trying to go to the thread and to the profile at the same time.

TL;DR

My app crashes because two NavigationLinks are triggered at the same time when they shouldn't.

How can I fix this?

Thanks in Advance.

My Code

/// The status is being displayed in a ``StatusList``, so we should make it smaller and more compact.
private struct CompactStatusView: View {

    /// The ``Status`` data model from where we obtain all the data.
    var status: Status

    /// Used to trigger the navectigationLink to redirect the user to the thread.
    @Binding var goToThread: Bool

    /// Used to redirect the user to a specific profile.
    @Binding var profileViewActive: Bool

    var body: some View {
        ZStack {

            self.content
                .padding(.vertical, 5)
                .contextMenu(
                    ContextMenu(menuItems: {

                        Button(action: {}, label: {
                            Label("Report post", systemImage: "flag")
                        })

                        Button(action: {}, label: {
                            Label("Report \(self.status.account.displayName)", systemImage: "flag")
                        })

                        Button(action: {}, label: {
                            Label("Share as Image", systemImage: "square.and.arrow.up")
                        })

                    })
                )

        }
            .buttonStyle(PlainButtonStyle())
            .navigationBarHidden(self.profileViewActive)
    }

    var content: some View {
        HStack(alignment: .top, spacing: 12) {

            URLImage(URL(string: self.status.account.avatarStatic)!,
                placeholder: { _ in
                    Image("amodrono")
                        .resizable()
                        .scaledToFit()
                        .clipShape(Circle())
                        .frame(width: 50, height: 50)
                        .redacted(reason: .placeholder)
                },
                content: {
                    $0.image
                        .resizable()
                        .scaledToFit()
                        .clipShape(Circle())
                        .frame(width: 50, height: 50)
                }
            )
                .onTapGesture {
                    self.profileViewActive.toggle()
                }
                .background(
                    NavigationLink(
                        destination: ProfileView(
                            accountInfo: ProfileViewModel(
                                accountID: self.status.account.id
                            ),
                            isParent: false
                        ),
                        isActive: self.$profileViewActive
                    ) {
                        Text("")
                    }
                        .frame(width: 0, height: 0)
                )

            VStack(alignment: .leading, spacing: 2) {
                HStack(alignment: .firstTextBaseline) {

                    if !self.status.account.displayName.isEmpty {
                        Text("\(self.status.account.displayName)")
                            .font(.headline)
                            .lineLimit(1)
                    }

                    Text("@\(self.status.account.acct)")
                        .foregroundColor(.secondary)
                        .lineLimit(1)

                    Text("· \(self.status.createdAt.getDate()!.getInterval())")
                        .foregroundColor(.secondary)
                        .lineLimit(1)
                }

                StatusViewContent(
                    isMain: false,
                    content: self.status.content,
                    card: self.status.card,
                    attachments: self.status.mediaAttachments,
                    goToProfile: self.$profileViewActive
                )

                StatusActionButtons(
                    isMain: false,
                    repliesCount: self.status.repliesCount,
                    reblogsCount: self.status.reblogsCount,
                    favouritesCount: self.status.favouritesCount,
                    statusUrl: self.status.uri
                )

            }
                .onTapGesture {
                    self.goToThread.toggle()
                }
                .background(
                    NavigationLink(
                        destination: ThreadView(
                            mainStatus: self.status
                        ),
                        isActive: self.$goToThread
                    ) {
                        EmptyView()
                    }
                )

            Spacer()
        }
    }

}

I guess the important parts here are:

The profile image:

URLImage(URL(string: self.status.account.avatarStatic)!,
                placeholder: { _ in
                    Image("amodrono")
                        .resizable()
                        .scaledToFit()
                        .clipShape(Circle())
                        .frame(width: 50, height: 50)
                        .redacted(reason: .placeholder)
                },
                content: {
                    $0.image
                        .resizable()
                        .scaledToFit()
                        .clipShape(Circle())
                        .frame(width: 50, height: 50)
                }
            )
                .onTapGesture {
                    self.profileViewActive.toggle()
                }
                .background(
                    NavigationLink(
                        destination: ProfileView(
                            accountInfo: ProfileViewModel(
                                accountID: self.status.account.id
                            ),
                            isParent: false
                        ),
                        isActive: self.$profileViewActive
                    ) {
                        Text("")
                    }
                        .frame(width: 0, height: 0)
                )

The two last modifiers

.onTapGesture {
                    self.goToThread.toggle()
                }
                .background(
                    NavigationLink(
                        destination: ThreadView(
                            mainStatus: self.status
                        ),
                        isActive: self.$goToThread
                    ) {
                        EmptyView()
                    }
                )

Upvotes: 2

Views: 997

Answers (1)

Asperi
Asperi

Reputation: 257711

Here is a demo of possible solution on some replicated scenario (because provided code is not testable as-is). The idea is to reuse one NavigationLink but with different destinations depending on activation place.

Tested with Xcode 12 / iOS 14

struct DemoRowView: View {
    @State private var isProfile = false
    @State private var isActive = false
    var body: some View {
        HStack {
            Image(systemName: "person")
                .scaledToFit()
                .onTapGesture {
                    self.isProfile = true
                    self.isActive = true
                }
            Text("Thread description")
                .onTapGesture {
                    self.isProfile = false
                    self.isActive = true
                }
                .background(
                    NavigationLink(destination: self.destination(), isActive: $isActive) { EmptyView() }
                )
        }
        .buttonStyle(PlainButtonStyle())
    }

    @ViewBuilder
    private func destination() -> some View {
        if isProfile {
            ProfileView()
        } else {
            ThreadView()
        }
    }
}

Upvotes: 2

Related Questions