はてなエンジニアアドベントカレンダー2025 50日目の記事です。
SwiftUIのNavigationSplitViewを使ってるアプリは多くありません。
また、詳細な解説記事も少なく、最初は自分も誤用していました。
【SwiftUI】NavigationSplitView誤用しててonAppear呼ばれなかった件
今回は、NavigationSplitViewとListを組み合わせた場合の挙動を見ていきます。
お題
まずは今回のお題を整理します。
環境
NavigationSplitView の2カラム表示
今回は簡略化のためNavigationSplitView.init(sidebar:detail:)を採用します。
sidebarにList表示、detailにList選択の詳細表示といった具合です。
NavigationSplitView.init(sidebar:content:detail:)は3カラムになりますが、利用方法は同じです。
Listは標準的すぎて扱いづらいという意見もありますが、プラットフォームごとのLook and Feelが適用されるので、好んで使用しています。
表示データ
表示用のデータは以下を用います。
Listに表示、selectionにBindする都合、HashableかつIdentifiableにしています。
struct Symbol: Hashable, Identifiable { var name: String var emoji: String var id: String { name } static var symbols: [Symbol] { [ Symbol(name: "Rat", emoji: "🐀"), Symbol(name: "Ox", emoji: "🐂"), Symbol(name: "Tiger", emoji: "🐅"), Symbol(name: "Rabbit", emoji: "🐇"), Symbol(name: "Dragon", emoji: "🐉"), Symbol(name: "Snake", emoji: "🐍"), Symbol(name: "Horse", emoji: "🐎"), Symbol(name: "Goat", emoji: "🐐"), Symbol(name: "Monkey", emoji: "🐒"), Symbol(name: "Rooster", emoji: "🐓"), Symbol(name: "Dog", emoji: "🐕"), Symbol(name: "Boar", emoji: "🐗"), ] } }
List表示とDetail遷移
表示と遷移を実装します。
実装
struct SplitView: View { @State private var selection: Symbol? var body: some View { NavigationSplitView { Sidebar(selection: $selection) } detail: { Detail(selection: selection) } } } [f:id:yutailang0119:20260117135652p:plain][f:id:yutailang0119:20260117141443p:plain] extension SplitView { struct Sidebar: View { private let symbols: [Symbol] = Symbol.symbols @Binding var selection: Symbol? var body: some View { List(symbols, id: \.self, selection: $selection) { Text($0.name) } } } struct Detail: View { let selection: Symbol? var body: some View { if let selection { Text(selection.emoji).font(.largeTitle) } else { ContentUnavailableView("Select from sidebar", systemImage: "pawprint") } } } }
実行
| Regularサイズ表示 | 選択状態 |
|---|---|
![]() |
![]() |
GIF

Detailへの表示とList.selectionをBindすると、選択状態がListに反映されます。
iPhone表示だと見えづらい部分ですが、表示条件分けを意識する必要がないのは素晴らしいですね。
Overviewにもある通りですが、List.selectionに適切にBindすると、List (またはその内部のForEach)に表示するRowContentはNavigationLinkにする必要はありません。
それどころかButtonでなくとも、Listがタップを判定してselectionに状態を伝えてくれます。
一点不明なのは、ドキュメントのサンプルにはない id: \.self, を指定しないと、Bindが動かなかったことです。
Identifiableなのですが...
List以外からの遷移
SidebarのToolbarからDetailに別画面を表示したいことは、典型的なユースケースです。
Detailの選択肢をenumで表現しましょう。
enum Selection: Equatable { case symbol(Symbol) case cats }
実装
型が異なり、直接@BindingにBindできない箇所には、Binding.init(get:set:)を使って変換します。
struct SplitView: View { @State private var selection: Selection? var body: some View { NavigationSplitView { Sidebar( selection: Binding { switch selection { case .symbol(let symbol): symbol case .cats, nil: nil } } set: { selection = $0.flatMap(Selection.symbol) } ) .toolbar { ToolbarItem(placement: .topBarLeading) { Button { selection = .cats } label: { Label("Cats", systemImage: "cat") } } } } detail: { Detail(selection: selection) } } } extension SplitView { struct Sidebar: View { private let symbols: [Symbol] = Symbol.symbols @Binding var selection: Symbol? var body: some View { List(symbols, id: \.self, selection: $selection) { Text($0.name) } } } struct Detail: View { let selection: Selection? var body: some View { switch selection { case .symbol(let symbol): Text(symbol.emoji).font(.largeTitle) case .cats: Text("🐈🐈<200d>⬛").font(.largeTitle) default: ContentUnavailableView("Select from sidebar", systemImage: "pawprint") } } } }
実行
| Regularサイズ表示 | 選択状態 |
|---|---|
![]() |
![]() |
しかし、Compact表示 (iPhone) ではToolbarのボタンからの遷移が機能しません。

Detail: ViewのonAppear(perform:)は呼ばれ、
onDisappear(perform:)は呼ばれていませんでした。
NavigationSplitViewの表示管理がうまくいっていないようです。
この場合はNavigationSplitViewColumnのState管理を、自分でやるとうまくいきます。
Binding<NavigationSplitViewColumn>
NavigationSplitView.init(preferredCompactColumn:sidebar:detail:)を使うと、外側からNavigationSplitViewColumnをBindできます。
preferredCompactColumn: Binding<NavigationSplitViewColumn> なので、変化に連動してNavigationSplitViewが表示するカラムを決定します。
実装
struct SplitView: View { @State private var preferredColumn: NavigationSplitViewColumn = .sidebar var body: some View { NavigationSplitView(preferredCompactColumn: $preferredColumn) { Sidebar(...) } detail: { Detail(...) } .onChange(of: selection) { _, newValue in preferredColumn = newValue == nil ? .sidebar : .detail } } }
onChange(of:initial:_:)で、preferredColumnを更新するのがポイントです。
これで selection = .cats の代入に合わせて、preferredColumn = .detail が行われ、画面遷移が動きます。
実行

まとめ
SwiftUI.NavigationSplitViewとListを組み合わせた画面遷移、選択状態の管理を解説しました。
NavigationSplitViewはクセが強いやつで、この他にも知らないと引っかかる罠が存在しますが、扱えるようになると選択できるUI表現の幅が広がります。
2026年はNavigationSplitViewが使われるアプリが増え、知見が広く共有されることを期待しています!
はてなエンジニアアドベントカレンダー2025
今年もお疲れ様でした。 はてなエンジニア Advent Calendar 2025 - Hatena Developer Blog
よいお年を!🐍🔜🐎
🐈🐈⬛



