iOS 16で導入されたNavigationStackは、SwiftUIのナビゲーション管理を根本から変える画期的な機能です。この記事では、NavigationStackの基礎から実践的な使い方まで、わかりやすく解説していきます。
目次
NavigationStackとは?
NavigationStackは、従来のNavigationViewに代わる新しいナビゲーション管理の仕組みです。より宣言的で、プログラムから制御しやすく、型安全なナビゲーションを実現します。
従来のNavigationViewとの違い
NavigationView(旧)
- ネストが複雑になりがち
- プログラムからの制御が困難
- Deep Linkの実装が煩雑
NavigationStack(新)
- シンプルで読みやすい構造
- スタック全体を外部から操作可能
- 型安全で保守性が高い
基本的な使い方
まずは、最もシンプルなNavigationStackから見ていきましょう。
import SwiftUI
struct BasicNavigationExample: View {
var body: some View {
NavigationStack {
List {
NavigationLink("りんご", value: "Apple")
NavigationLink("バナナ", value: "Banana")
NavigationLink("さくらんぼ", value: "Cherry")
}
.navigationTitle("果物リスト")
.navigationDestination(for: String.self) { fruit in
VStack {
Image(systemName: "leaf.fill")
.font(.system(size: 100))
.foregroundColor(.green)
Text(fruit)
.font(.largeTitle)
.padding()
}
.navigationTitle(fruit)
}
}
}
}
このコードのポイントは以下の通りです:
- NavigationLinkに画面ではなく「値」を渡す
- .navigationDestinationで、その値の型に応じた画面を定義
- 値と画面の結びつけを分離できる
NavigationPathでスタックを管理する
NavigationStackの真の力は、NavigationPathを使った状態管理にあります。
import SwiftUI
struct PathManagedNavigation: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
VStack(spacing: 20) {
Text("現在のスタック深度: \(path.count)")
.font(.headline)
Button("画面1へ") {
path.append("Screen1")
}
Button("画面2へ直接ジャンプ") {
path.append("Screen1")
path.append("Screen2")
}
Button("3階層深くジャンプ") {
path.append("Screen1")
path.append("Screen2")
path.append("Screen3")
}
if path.count > 0 {
Button("ルートに戻る") {
path.removeLast(path.count)
}
.foregroundColor(.red)
}
}
.navigationTitle("ホーム")
.navigationDestination(for: String.self) { screen in
ScreenView(screenName: screen, path: $path)
}
}
}
}
struct ScreenView: View {
let screenName: String
@Binding var path: NavigationPath
var body: some View {
VStack(spacing: 20) {
Text(screenName)
.font(.largeTitle)
Text("スタック深度: \(path.count)")
.foregroundColor(.secondary)
Button("次の画面へ") {
path.append("\(screenName) -> Next")
}
Button("ルートに戻る") {
path.removeLast(path.count)
}
.foregroundColor(.red)
}
.navigationTitle(screenName)
}
}
このコードでは:
- プログラムから自由にナビゲーションスタックを操作
- 任意の階層へ一気にジャンプ
- ルートへ一発で戻る
- 現在のスタック状態を監視
といったことが簡単に実現できます。
型安全な配列でパスを管理
より型安全な方法として、具体的な型の配列でパスを管理することもできます。
import SwiftUI
enum Destination: Hashable {
case profile(User)
case settings
case detail(ItemDetail)
}
struct User: Hashable {
let id: Int
let name: String
}
struct ItemDetail: Hashable {
let id: Int
let title: String
}
struct TypeSafeNavigation: View {
@State private var path: [Destination] = []
var body: some View {
NavigationStack(path: $path) {
List {
Button("プロフィールを見る") {
path.append(.profile(User(id: 1, name: "太郎")))
}
Button("設定画面へ") {
path.append(.settings)
}
Button("詳細を見る") {
path.append(.detail(ItemDetail(id: 1, title: "商品A")))
}
}
.navigationTitle("メニュー")
.navigationDestination(for: Destination.self) { destination in
destinationView(for: destination)
}
}
}
@ViewBuilder
func destinationView(for destination: Destination) -> some View {
switch destination {
case .profile(let user):
VStack {
Image(systemName: "person.circle.fill")
.font(.system(size: 100))
Text(user.name)
.font(.title)
}
.navigationTitle("プロフィール")
case .settings:
Text("設定画面")
.navigationTitle("設定")
case .detail(let item):
VStack {
Text(item.title)
.font(.title)
Text("ID: \(item.id)")
.foregroundColor(.secondary)
}
.navigationTitle("詳細")
}
}
}
列挙型を使うことで:
- コンパイル時に型チェック
- 画面遷移のパターンが明確
- リファクタリングが容易
- バグを未然に防止
といったメリットが得られます。
実践的な例:Todo アプリ
実際のアプリケーションでの使用例を見てみましょう。
import SwiftUI
struct Todo: Identifiable, Hashable {
let id = UUID()
var title: String
var isCompleted: Bool
}
struct TodoApp: View {
@State private var todos = [
Todo(title: "SwiftUIを学ぶ", isCompleted: false),
Todo(title: "NavigationStackを理解する", isCompleted: false),
Todo(title: "アプリを作る", isCompleted: false)
]
@State private var path: [Todo] = []
var body: some View {
NavigationStack(path: $path) {
List {
ForEach(todos) { todo in
NavigationLink(value: todo) {
HStack {
Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundColor(todo.isCompleted ? .green : .gray)
Text(todo.title)
.strikethrough(todo.isCompleted)
}
}
}
}
.navigationTitle("Todoリスト")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("全て完了") {
for index in todos.indices {
todos[index].isCompleted = true
}
}
}
}
.navigationDestination(for: Todo.self) { todo in
TodoDetailView(todo: todo, path: $path)
}
}
}
}
struct TodoDetailView: View {
let todo: Todo
@Binding var path: [Todo]
var body: some View {
VStack(spacing: 30) {
Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
.font(.system(size: 100))
.foregroundColor(todo.isCompleted ? .green : .gray)
Text(todo.title)
.font(.title)
Text(todo.isCompleted ? "完了済み" : "未完了")
.font(.headline)
.foregroundColor(todo.isCompleted ? .green : .orange)
Spacer()
Button("リストに戻る") {
path.removeAll()
}
.buttonStyle(.borderedProminent)
}
.padding()
.navigationTitle("Todo詳細")
}
}
このTodoアプリの例では:
- リストから詳細画面への遷移
- カスタムオブジェクトを値として使用
- 詳細画面からリストへ直接戻る機能
- ツールバーでの一括操作
といった実用的な機能を実装しています。
Deep Linkの実装
NavigationStackを使えば、Deep Link(特定の画面に直接遷移)も簡単に実装できます。
import SwiftUI
struct DeepLinkExample: View {
@State private var path: [Int] = []
var body: some View {
NavigationStack(path: $path) {
VStack(spacing: 20) {
Button("記事1を開く") {
openArticle(id: 1)
}
Button("記事5を開く") {
openArticle(id: 5)
}
Button("記事10の詳細を開く") {
openArticleDetail(id: 10)
}
}
.navigationTitle("記事リスト")
.navigationDestination(for: Int.self) { articleId in
ArticleView(id: articleId, path: $path)
}
}
.onOpenURL { url in
handleDeepLink(url)
}
}
func openArticle(id: Int) {
path = [id]
}
func openArticleDetail(id: Int) {
path = [id, -id] // -idで詳細画面を表現
}
func handleDeepLink(_ url: URL) {
// 例: myapp://article/5
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
let articleId = Int(components.path.replacingOccurrences(of: "/article/", with: "")) {
openArticle(id: articleId)
}
}
}
struct ArticleView: View {
let id: Int
@Binding var path: [Int]
var body: some View {
VStack {
Text("記事 \(id)")
.font(.title)
Button("詳細を見る") {
path.append(-id)
}
}
.navigationTitle("記事 \(id)")
}
}
まとめ
NavigationStackは、SwiftUIのナビゲーションをより強力で使いやすくする機能です。
主なメリット:
- プログラムから完全に制御可能
- 型安全で保守性が高い
- Deep Linkの実装が容易
- コードがシンプルで読みやすい
使い分けのポイント:
- シンプルなアプリ → NavigationPathで十分
- 複雑なナビゲーション → 型付き配列を使用
- Deep Link対応 → pathを外部から設定
iOS 16以降をターゲットにするアプリでは、積極的にNavigationStackを採用することをお勧めします。従来のNavigationViewよりも、はるかに柔軟で強力なナビゲーション管理が実現できます。