😊

【SwiftUI】NavigationSplitView誤用しててonAppear呼ばれなかった件

2025/02/19に公開

個人開発でハマった件を書きます。

https://github.com/0si43/PiecesOfPaper

前提

Pieces of Paperはサイドバー方式で、ノート一覧を表示しています。

このようなUIを実現するためには、SwiftUIの List を使うんですが、
サイドバーで画面を切り替えても onAppear が呼ばれない、という現象が起きていました。

ちゃんとドキュメント読んだ人だったら、当然回避できている問題かもしれませんが、共有で書いておきます。

NavigationSplitViewへの移行

iOS 16からSwiftUIのNavigationまわりが大幅変更されまして、本アプリも移行しました。
NavigationView がdeprecatedとなり、 NavigationSplitView を使うことになりました。

https://developer.apple.com/documentation/swiftui/navigationsplitview

具体的には、このような変更をかけました。

SideBarListView
    var body: some View {
        if #available(iOS 16.0, *) {
            NavigationSplitView() {
                list
            } detail: {
                Notes(viewModel: inboxNoteViewModel)
            }
        } else {
            NavigationView {
                list
            }
        }
    }

    var list: some View {
        List {
            Section(header: Text("Folder")) {
                NavigationLink(destination: Notes(viewModel: inboxNoteViewModel),
                               isActive: $isActive) {
                    Label("Inbox", systemImage: "tray")
                }
                // …
            }
            // …
        }
        .navigationTitle("Pieces of Paper")
    }

この時点で NavigationSplitView を誤用していました。

正しい使い方

マイグレーションガイドをナナメ読みして変更かけたんですが、誤解していました。
サンプルコードだとこうなっています。

let colors: [Color] = [.purple, .pink, .orange]
@State private var selection: Color? = nil // Nothing selected by default.

var body: some View {
    NavigationSplitView {
        List(colors, id: \.self, selection: $selection) { color in
            NavigationLink(color.description, value: color)
        }
    } detail: {
        if let color = selection {
            ColorDetail(color: color)
        } else {
            Text("Pick a color")
        }
    }
}

State変数 selection を持っていて、第二引数detailのクロージャー中で参照します。
第一引数はあくまでサイドバーで表示するコンテンツと、State変数の更新だけを行います。

不具合を仕込んだ箇所

さて、最初のケースに戻りましょう。

NavigationSplitView() {
    list
} detail: {
    Notes(viewModel: inboxNoteViewModel)
}

var list: some View {
    List {
        Section(header: Text("Folder")) {
            NavigationLink(destination: Notes(viewModel: inboxNoteViewModel),
                           isActive: $isActive) {
                Label("Inbox", systemImage: "tray")
            }
// …
    }
}

この指定だと、detailには最初に表示されるView、その後サイドバーの操作で切り替える際は NavigationLink(destination:) を使っていました。
厄介なのはこの指定でもページは切り替わります。

ただonAppearは呼ばれません。
ListのUIが、iOSとiPadOSで変化するので、iOSだと呼ばれたような気がします。

学び

NavigationSplitView x NavigationLink(destination:)はなんとなく動作しますが、正しい指定ではなさそうなので避けましょう。

(了)

Discussion