🎨

【SwiftUI】ToolbarItem(placement: .principal)にNavigationLinkを配置する際の注意点

2024/06/13に公開

NavigationViewを使用している場合、ToolbarItem(placement: .principal)NavigationLinkを配置すると画面遷移が発火しないケースがありました。

画面遷移しないパターン

以下のように、ToolbarItem(placement: .principal)内のNavigationLink
条件によって表示・非表示を切り替えられるようにすると、画面遷移が全く発火しなくなりました。

import SwiftUI

struct NavigationViewSampleView: View {
    @State private var showItem = false

    var body: some View {
        NavigationView {
            VStack {
                Button(showItem ? "hide" : "show") {
                    showItem.toggle()
                }
            }
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .principal) {
                    if showItem {
                        NavigationLink("principal") {
                            Text("principal tapped")
                        }
                    }
                }
                ToolbarItem(placement: .primaryAction) {
                    if showItem {
                        NavigationLink("menu") {
                            Text("menu tapped")
                        }
                    }
                }
            }
        }
    }
}

条件分岐がなければ正しく画面遷移されますし、.primaryActionの方は条件分岐があっても遷移するため、
特定条件下でのみ発生するバグかと思われます。

画面遷移するように修正する

ToolbarItem(placement: .principal)内でNavigationLinkを使わず、
isActiveで遷移を管理するNavigationLinkを使えばこの問題は発生しなくなりました。

import SwiftUI

struct NavigationViewSampleView: View {
    @State private var showItem = false
    @State private var principalTapped = false

    var body: some View {
        NavigationView {
            VStack {
                Button(showItem ? "hide" : "show") {
                    showItem.toggle()
                }
            }
            .background {
                NavigationLink(isActive: $principalTapped) {
                    Text("principal tapped")
                } label: {
                    EmptyView()
                }
            }
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .principal) {
                    if showItem {
                        Button("principal") {
                            principalTapped = true
                        }
                    }
                }
                ToolbarItem(placement: .primaryAction) {
                    if showItem {
                        NavigationLink("menu") {
                            Text("menu tapped")
                        }
                    }
                }
            }
        }
    }
}

また、NavigationStackを使っていれば、ToolbarItem(placement: .principal)内でNavigationLinkの表示制御をしていても問題なく画面遷移ができました。

import SwiftUI

struct NavigationStackSampleView: View {
    @State private var showToolbar = false

    var body: some View {
        NavigationStack {
            VStack {
                Text("NavigationStackSampleView")
                Button(showToolbar ? "hide" : "show") {
                    showToolbar.toggle()
                }
            }
            .toolbar {
                ToolbarItem(placement: .principal) {
                    if showToolbar {
                        NavigationLink("principal", value: "principal")
                    }
                }
            }
            .navigationDestination(for: String.self) {
                Text($0 + " tapped")
            }
        }
    }
}

isActiveを用いたNavigationLinkは現在deprecatedになっているため、
iOS 16以降をターゲットとしたアプリであれば、素直にNavigationStackを使った方が良いと思います。

Discussion