MENU

【SwiftUI入門】NavigationViewの使い方完全ガイド!画面遷移からツールバーまで徹底解説

SwiftUIでアプリを開発する際、画面遷移(ナビゲーション)の実装は必須スキルです。この記事では、NavigationViewの基本から実践的な使い方まで、初心者にもわかりやすく解説します。

目次

NavigationViewとは?

NavigationViewは、SwiftUIで画面遷移機能を実装するためのコンテナビューです。iOSアプリでよく見る、以下のような機能を簡単に実装できます。

  • 画面上部のタイトルバー
  • 画面間の遷移(次の画面へ進む)
  • 自動で表示される戻るボタン
  • ツールバーのボタン

NavigationViewとNavigationStackの違い

重要なお知らせ: iOS 16以降ではNavigationStackが推奨されていますが、この記事では両方を解説します。

項目NavigationViewNavigationStack
対応OSiOS 13以降iOS 16以降
状態レガシー(古い方式)最新の推奨方式
使いやすさやや複雑よりシンプル

まずは基本となるNavigationViewをマスターしましょう。

NavigationViewの基本的な使い方

最もシンプルな例

まずは、タイトルバーを表示する最小限のコードから始めましょう。

import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationView {
            Text("これがメイン画面です")
                .navigationTitle("ホーム")
        }
    }
}

このコードを実行すると、画面上部に「ホーム」というタイトルが表示されます。NavigationViewで囲むだけで、ナビゲーションバーが自動的に追加されます。

タイトルの表示スタイルを変更する

タイトルの表示方法は3種類から選べます。

struct ContentView: View {
    var body: some View {
        NavigationView {
            List(1..<30) { index in
                Text("アイテム \(index)")
            }
            .navigationTitle("リスト")
            .navigationBarTitleDisplayMode(.large)
        }
    }
}

3つの表示モード:

  1. .large – 大きく表示され、スクロールすると自動的に小さくなる(デフォルト)
  2. .inline – 常に小さく表示される
  3. .automatic – システムが自動的に判断

実際にスクロールしてみると、.largeの場合はタイトルが縮小するアニメーションが見られます。

画面遷移の実装:NavigationLink

NavigationLinkを使うと、タップで次の画面に遷移できます。

基本的な画面遷移

struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                // シンプルな遷移
                NavigationLink("詳細画面へ移動") {
                    DetailView()
                }
                
                // カスタムデザインの遷移ボタン
                NavigationLink {
                    SettingsView()
                } label: {
                    Text("設定画面へ")
                        .foregroundColor(.white)
                        .padding()
                        .background(Color.blue)
                        .cornerRadius(10)
                }
            }
            .navigationTitle("メニュー")
        }
    }
}

struct DetailView: View {
    var body: some View {
        Text("ここは詳細画面です")
            .font(.title)
            .navigationTitle("詳細")
    }
}

struct SettingsView: View {
    var body: some View {
        Text("ここは設定画面です")
            .font(.title)
            .navigationTitle("設定")
    }
}

ポイント:

  • NavigationLinkをタップすると自動的に次の画面に遷移
  • 戻るボタンも自動で表示される
  • 遷移先の画面でも.navigationTitle()でタイトルを設定できる

Listと組み合わせた実装

最も一般的な使い方は、リストと組み合わせた画面遷移です。

struct FruitListView: View {
    let fruits = ["りんご", "バナナ", "オレンジ", "ぶどう", "メロン", "いちご"]
    
    var body: some View {
        NavigationView {
            List(fruits, id: \.self) { fruit in
                NavigationLink(fruit) {
                    FruitDetailView(fruitName: fruit)
                }
            }
            .navigationTitle("果物リスト")
        }
    }
}

struct FruitDetailView: View {
    let fruitName: String
    
    var body: some View {
        VStack(spacing: 20) {
            Image(systemName: "leaf.fill")
                .font(.system(size: 80))
                .foregroundColor(.green)
            
            Text(fruitName)
                .font(.largeTitle)
                .bold()
            
            Text("の詳細情報がここに表示されます")
                .foregroundColor(.gray)
        }
        .navigationTitle(fruitName)
    }
}

この例では、果物のリストをタップすると、その果物の詳細画面に遷移します。

データを渡して画面遷移

実際のアプリでは、データを次の画面に渡すことがよくあります。

// データモデル
struct Product: Identifiable {
    let id = UUID()
    let name: String
    let price: Int
    let description: String
}

struct ProductListView: View {
    let products = [
        Product(name: "ノートPC", price: 120000, description: "高性能なノートパソコン"),
        Product(name: "スマートフォン", price: 80000, description: "最新のスマートフォン"),
        Product(name: "タブレット", price: 60000, description: "便利なタブレット"),
        Product(name: "スマートウォッチ", price: 45000, description: "健康管理に最適")
    ]
    
    var body: some View {
        NavigationView {
            List(products) { product in
                NavigationLink {
                    ProductDetailView(product: product)
                } label: {
                    HStack {
                        VStack(alignment: .leading) {
                            Text(product.name)
                                .font(.headline)
                            Text(product.description)
                                .font(.caption)
                                .foregroundColor(.gray)
                        }
                        
                        Spacer()
                        
                        Text("¥\(product.price)")
                            .font(.subheadline)
                            .bold()
                            .foregroundColor(.blue)
                    }
                    .padding(.vertical, 4)
                }
            }
            .navigationTitle("商品一覧")
        }
    }
}

struct ProductDetailView: View {
    let product: Product
    
    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 24) {
                // 商品画像エリア
                RoundedRectangle(cornerRadius: 12)
                    .fill(Color.gray.opacity(0.2))
                    .frame(height: 250)
                    .overlay(
                        Image(systemName: "photo")
                            .font(.system(size: 60))
                            .foregroundColor(.gray)
                    )
                
                // 商品情報
                VStack(alignment: .leading, spacing: 12) {
                    Text(product.name)
                        .font(.title)
                        .bold()
                    
                    Text("¥\(product.price)")
                        .font(.title2)
                        .foregroundColor(.blue)
                        .bold()
                    
                    Divider()
                    
                    Text("商品説明")
                        .font(.headline)
                    
                    Text(product.description)
                        .foregroundColor(.gray)
                }
                .padding(.horizontal)
                
                // 購入ボタン
                Button {
                    print("\(product.name)をカートに追加")
                } label: {
                    Text("カートに追加")
                        .font(.headline)
                        .foregroundColor(.white)
                        .frame(maxWidth: .infinity)
                        .padding()
                        .background(Color.blue)
                        .cornerRadius(10)
                }
                .padding(.horizontal)
            }
        }
        .navigationTitle(product.name)
        .navigationBarTitleDisplayMode(.inline)
    }
}

この例では、商品リストから商品をタップすると、その商品の詳細情報が次の画面に渡されて表示されます。

ツールバーの実装

ナビゲーションバーにボタンを追加する方法を見ていきましょう。

基本的なツールバーボタン

struct ContentView: View {
    var body: some View {
        NavigationView {
            Text("メイン画面")
                .navigationTitle("ホーム")
                .toolbar {
                    // 左側にボタンを配置
                    ToolbarItem(placement: .navigationBarLeading) {
                        Button("編集") {
                            print("編集ボタンがタップされました")
                        }
                    }
                    
                    // 右側にボタンを配置
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button {
                            print("追加ボタンがタップされました")
                        } label: {
                            Image(systemName: "plus")
                        }
                    }
                }
        }
    }
}

複数のボタンを配置

右側に複数のボタンを並べることもできます。

struct ContentView: View {
    @State private var showingSearch = false
    
    var body: some View {
        NavigationView {
            Text("メイン画面")
                .navigationTitle("ホーム")
                .toolbar {
                    ToolbarItemGroup(placement: .navigationBarTrailing) {
                        Button {
                            showingSearch = true
                            print("検索")
                        } label: {
                            Image(systemName: "magnifyingglass")
                        }
                        
                        Button {
                            print("フィルター")
                        } label: {
                            Image(systemName: "line.3.horizontal.decrease.circle")
                        }
                        
                        Button {
                            print("設定")
                        } label: {
                            Image(systemName: "gearshape")
                        }
                    }
                }
        }
    }
}

プログラムで画面遷移を制御

ボタン以外の条件で画面遷移をトリガーしたい場合もあります。

struct LoginView: View {
    @State private var username = ""
    @State private var password = ""
    @State private var isLoggedIn = false
    
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                TextField("ユーザー名", text: $username)
                    .textFieldStyle(.roundedBorder)
                    .autocapitalization(.none)
                
                SecureField("パスワード", text: $password)
                    .textFieldStyle(.roundedBorder)
                
                Button("ログイン") {
                    // ログイン処理
                    if !username.isEmpty && !password.isEmpty {
                        isLoggedIn = true
                    }
                }
                .buttonStyle(.borderedProminent)
                .disabled(username.isEmpty || password.isEmpty)
                
                // 非表示のNavigationLink
                NavigationLink(isActive: $isLoggedIn) {
                    HomeView(username: username)
                } label: {
                    EmptyView()
                }
            }
            .padding()
            .navigationTitle("ログイン")
        }
    }
}

struct HomeView: View {
    let username: String
    
    var body: some View {
        VStack {
            Text("ようこそ、\(username)さん!")
                .font(.title)
        }
        .navigationTitle("ホーム")
        .navigationBarBackButtonHidden(true)  // 戻るボタンを非表示
    }
}

このパターンは、ログイン成功後に自動的にホーム画面に遷移させたい場合などに便利です。

実践例:タブ付き設定画面

実際のアプリでよく見る設定画面を作ってみましょう。

struct SettingsView: View {
    @State private var notificationsEnabled = true
    @State private var darkModeEnabled = false
    @State private var fontSize = 16.0
    
    var body: some View {
        NavigationView {
            Form {
                Section("通知設定") {
                    Toggle("プッシュ通知", isOn: $notificationsEnabled)
                    
                    if notificationsEnabled {
                        NavigationLink("通知の詳細設定") {
                            NotificationSettingsView()
                        }
                    }
                }
                
                Section("外観") {
                    Toggle("ダークモード", isOn: $darkModeEnabled)
                    
                    VStack(alignment: .leading) {
                        Text("フォントサイズ: \(Int(fontSize))pt")
                        Slider(value: $fontSize, in: 12...24, step: 1)
                    }
                }
                
                Section("アカウント") {
                    NavigationLink("プロフィール") {
                        ProfileView()
                    }
                    
                    NavigationLink("プライバシー") {
                        PrivacyView()
                    }
                    
                    NavigationLink("セキュリティ") {
                        SecurityView()
                    }
                }
                
                Section("サポート") {
                    NavigationLink("ヘルプ") {
                        HelpView()
                    }
                    
                    NavigationLink("お問い合わせ") {
                        ContactView()
                    }
                    
                    NavigationLink("バージョン情報") {
                        VersionView()
                    }
                }
                
                Section {
                    Button("ログアウト") {
                        print("ログアウト処理")
                    }
                    .foregroundColor(.red)
                }
            }
            .navigationTitle("設定")
        }
    }
}

struct NotificationSettingsView: View {
    @State private var emailNotifications = true
    @State private var smsNotifications = false
    
    var body: some View {
        Form {
            Toggle("メール通知", isOn: $emailNotifications)
            Toggle("SMS通知", isOn: $smsNotifications)
        }
        .navigationTitle("通知の詳細")
    }
}

struct ProfileView: View {
    var body: some View {
        VStack(spacing: 20) {
            Image(systemName: "person.circle.fill")
                .font(.system(size: 100))
                .foregroundColor(.blue)
            
            Text("山田太郎")
                .font(.title)
            
            Text("taro@example.com")
                .foregroundColor(.gray)
            
            Button("プロフィールを編集") {
                print("編集画面へ")
            }
            .buttonStyle(.borderedProminent)
        }
        .navigationTitle("プロフィール")
    }
}

struct PrivacyView: View {
    var body: some View {
        Form {
            Section("データ収集") {
                Toggle("使用状況データ", isOn: .constant(true))
                Toggle("クラッシュレポート", isOn: .constant(true))
            }
            
            Section("位置情報") {
                Text("常に許可")
            }
        }
        .navigationTitle("プライバシー")
    }
}

struct SecurityView: View {
    var body: some View {
        Form {
            Section("認証") {
                Toggle("Face ID", isOn: .constant(true))
                Toggle("2段階認証", isOn: .constant(false))
            }
            
            Section {
                Button("パスワードを変更") {
                    print("パスワード変更")
                }
            }
        }
        .navigationTitle("セキュリティ")
    }
}

struct HelpView: View {
    var body: some View {
        List {
            NavigationLink("はじめに") {
                Text("はじめにの内容")
            }
            NavigationLink("よくある質問") {
                Text("FAQの内容")
            }
            NavigationLink("使い方ガイド") {
                Text("ガイドの内容")
            }
        }
        .navigationTitle("ヘルプ")
    }
}

struct ContactView: View {
    @State private var message = ""
    
    var body: some View {
        VStack {
            TextEditor(text: $message)
                .frame(height: 200)
                .padding()
                .overlay(
                    RoundedRectangle(cornerRadius: 8)
                        .stroke(Color.gray.opacity(0.5))
                )
                .padding()
            
            Button("送信") {
                print("お問い合わせ送信: \(message)")
            }
            .buttonStyle(.borderedProminent)
            .disabled(message.isEmpty)
            
            Spacer()
        }
        .navigationTitle("お問い合わせ")
    }
}

struct VersionView: View {
    var body: some View {
        VStack(spacing: 16) {
            Image(systemName: "app.badge")
                .font(.system(size: 80))
                .foregroundColor(.blue)
            
            Text("MyApp")
                .font(.title)
            
            Text("バージョン 1.0.0")
                .font(.headline)
                .foregroundColor(.gray)
            
            Text("ビルド 100")
                .font(.caption)
                .foregroundColor(.gray)
        }
        .navigationTitle("バージョン情報")
    }
}

NavigationBarの非表示とカスタマイズ

ナビゲーションバーを非表示にする

struct ContentView: View {
    var body: some View {
        NavigationView {
            Text("ナビゲーションバーなし")
                .navigationBarHidden(true)
        }
    }
}

戻るボタンを非表示にする

struct DetailView: View {
    var body: some View {
        Text("戻るボタンなし")
            .navigationTitle("詳細")
            .navigationBarBackButtonHidden(true)
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    Button("閉じる") {
                        // カスタムの閉じる処理
                    }
                }
            }
    }
}

カスタム戻るボタン

struct CustomBackButtonView: View {
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        Text("カスタム戻るボタン")
            .navigationBarBackButtonHidden(true)
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    Button {
                        dismiss()
                    } label: {
                        HStack {
                            Image(systemName: "chevron.left")
                            Text("戻る")
                        }
                    }
                }
            }
    }
}

iPadでの2カラムレイアウト

iPadでは、サイドバーとメインコンテンツの2カラムレイアウトが実装できます。

struct ContentView: View {
    var body: some View {
        NavigationView {
            // サイドバー
            List {
                NavigationLink("ホーム") {
                    HomeContentView()
                }
                NavigationLink("お気に入り") {
                    FavoritesContentView()
                }
                NavigationLink("設定") {
                    SettingsContentView()
                }
            }
            .navigationTitle("メニュー")
            
            // 初期表示される画面(iPadの右側)
            Text("左のメニューから項目を選択してください")
                .font(.title)
                .foregroundColor(.gray)
        }
    }
}

struct HomeContentView: View {
    var body: some View {
        Text("ホームの内容")
            .navigationTitle("ホーム")
    }
}

struct FavoritesContentView: View {
    var body: some View {
        Text("お気に入りの内容")
            .navigationTitle("お気に入り")
    }
}

struct SettingsContentView: View {
    var body: some View {
        Text("設定の内容")
            .navigationTitle("設定")
    }
}

iOS 16以降:NavigationStackへの移行

iOS 16以降では、NavigationStackの使用が推奨されています。基本的な使い方はNavigationViewと似ています。

struct ModernNavigationView: View {
    var body: some View {
        NavigationStack {
            List(1..<20) { index in
                NavigationLink("アイテム \(index)") {
                    Text("詳細 \(index)")
                        .navigationTitle("詳細")
                }
            }
            .navigationTitle("NavigationStack")
        }
    }
}

NavigationStackの利点

  • より効率的なメモリ管理
  • パスベースのナビゲーション
  • プログラマティックなナビゲーションが簡単
struct PathBasedNavigationView: View {
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            List(1..<10) { index in
                Button("アイテム \(index)") {
                    path.append(index)
                }
            }
            .navigationDestination(for: Int.self) { value in
                DetailView(value: value, path: $path)
            }
            .navigationTitle("ホーム")
        }
    }
}

struct DetailView: View {
    let value: Int
    @Binding var path: NavigationPath
    
    var body: some View {
        VStack(spacing: 20) {
            Text("アイテム \(value)")
                .font(.title)
            
            Button("ルートに戻る") {
                path = NavigationPath()
            }
            .buttonStyle(.borderedProminent)
        }
        .navigationTitle("詳細 \(value)")
    }
}

よくある問題と解決方法

問題1:NavigationLinkが反応しない

原因: NavigationViewで囲まれていない

解決方法:

// 悪い例
NavigationLink("次へ") {
    Text("詳細")
}

// 良い例
NavigationView {
    NavigationLink("次へ") {
        Text("詳細")
    }
}

問題2:戻るボタンが表示されない

原因: navigationBarBackButtonHidden(true)が設定されている

解決方法:

// この設定を削除するか、カスタムボタンを追加
.navigationBarBackButtonHidden(false)

問題3:タイトルが表示されない

原因: .navigationTitle()がNavigationViewの外にある

解決方法:

// 悪い例
NavigationView {
    Text("内容")
}
.navigationTitle("タイトル")

// 良い例
NavigationView {
    Text("内容")
        .navigationTitle("タイトル")
}

ベストプラクティス

1. iOS 16以降ではNavigationStackを使う

新しいプロジェクトではNavigationStackを優先しましょう。

2. タイトルは各画面で設定する

遷移先の画面でも.navigationTitle()を設定することで、わかりやすいUIになります。

3. 深い階層は避ける

ユーザーが迷わないよう、ナビゲーション階層は3〜4段階までに抑えましょう。

4. 適切なツールバーボタンを配置

よく使う機能は右上に、破壊的な操作(削除など)は左上に配置するのが一般的です。

まとめ:NavigationViewをマスターしよう

この記事では、SwiftUIのNavigationViewについて詳しく解説しました。

重要ポイントの復習

  1. 基本機能
    • 画面遷移とタイトルバーの実装
    • NavigationLinkで簡単に遷移
    • 自動で戻るボタンが表示される
  2. 画面遷移
    • シンプルなリンク
    • データを渡す遷移
    • プログラムでの遷移制御
  3. ツールバー
    • ボタンの追加と配置
    • 複数ボタンの実装
    • カスタムデザイン
  4. iOS 16以降
    • NavigationStackへの移行推奨
    • よりシンプルで効率的
    • パスベースのナビゲーション
  5. 実践テクニック
    • 設定画面の実装
    • iPadの2カラムレイアウト
    • カスタム戻るボタン

次のステップ

  • まずは基本的な画面遷移を実装
  • Listとの組み合わせで実用的なUIを作成
  • iOS 16以降の場合はNavigationStackを試す
  • ツールバーやカスタマイズを追加

NavigationViewは、SwiftUIアプリ開発の基礎となる重要な機能です。この記事で学んだことを実際のプロジェクトで試して、ナビゲーション機能をマスターしましょう!

プログラミングの独学におすすめ
プログラミング言語の人気オンラインコース
独学でプログラミングを学習している方で、エラーなどが発生して効率よく勉強ができないと悩む方は多いはず。Udemyは、プロの講師が動画で実際のプログラムを動かしながら教えてくれるオンライン講座です。講座の価格は、セール期間中には専門書籍を1冊買うよりも安く済むことが多いです。新しく学びたいプログラミング言語がある方は、ぜひUdemyでオンライン講座を探してみてください。
目次