38
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SwiftUIAdvent Calendar 2022

Day 8

NavigationStackを使ってみた🐥

Last updated at Posted at 2022-12-07

はじめに

SwiftUI Advent Calendar 2022の8日目です🎄

iOS 16からNavigationStackという新しい画面遷移のViewが使えるようになりました🐥
これにより画面遷移の実装がより簡単になったと日々の個人開発の中で実感しております。

NavigationStackの使い方を模索しておりまして、一旦現状を共有することで皆さんに助言をいただきより良いものにしようとする魂胆です。
ですのでこうした方が良いよ!とかなんでこんな書き方しているの?とかご意見ありましたら、ぜひコメントください!

また投稿前日に書いているため支離滅裂な箇所もあるかと思いますので生暖かい目で見ていただければと思います🙏

NavigationStackを使ってモリモリ開発しているアプリが気になる方はこちらから🎟
https://apps.apple.com/jp/app/ticketmania/id6444087177
(レビューください⭐️)

サンプルリポジトリはこちら
https://github.com/tsuzukihashi/SampleNavigationStack

動作環境

  • Xcode 12
  • iOS 16対象

NavigationStackとは

ざっくりいうとSwiftUIでも画面遷移をよりプログラムから制御しやすくしようとしたNavitaionViewの改良版的なものだと捉えています。

今回の記事では基本的な使い方については解説しないので詳しくはAppleのドキュメントを見てください🍎
Apple Developer Documentation

登場人物紹介(※独自実装)

  • AppEnvironment
    • EnvironmentObjectとして定義
    • アプリの画面遷移のデータを全て持つ縁の下の力持ち
  • Route
    • NavigationStackに与える値
    • 全ての遷移方法を知っている人物知り
  • NavigationModifier
    • NavigationStackのdestinationを一元管理する
    • コードがバラバラにならないようにまとめてくれる優しい人

AppEnvironmentの例

class AppEnvironment: ObservableObject {
    @Published var homePath: [Route] = []
    @Published var searchPath: [Route] = []
    @Published var myPagePath: [Route] = []
}

色々なViewから触りやすいようにEnvironmentObjectとして定義しています。
以下に定義するRouteの配列を持たせています。

Routeの例

enum Route: Hashable {
    case detail
    case user(id: String)
    case web(url: URL)
}

画面遷移のパターンをenumで定義しています。

Hashableに準拠させることでNavigationStackのpathに使うことができます。

画面遷移の情報をここで一元管理できるのがおすすめポイントです!

NavigationModifier

struct NavigationModifier: ViewModifier {
    @Binding var path: [Route]

    @ViewBuilder
    fileprivate func coordinator(_ route: Route) -> some View {
        switch route {
        case .detail:
            DetailView()
        case .user(let id):
            UserView(id: id)
        case .web(let url):
            WebView(url: url)
        }
    }

    func body(content: Content) -> some View {
        NavigationStack(path: $path) {
            content
                .navigationDestination(for: Route.self) { route in
                    coordinator(route)
                }
        }
    }
}

extension View {
    func navigation(path: Binding<[Route]>) -> some View {
        self.modifier(NavigationModifier(path: path))
    }
}

Modifierを使ってNavigationStackの実装を共通化しています。

以下のように使います。

struct HomeView: View {
    @EnvironmentObject var appEnvironment: AppEnvironment
    
    var body: some View {
        List {
            NavigationLink(value: Route.detail) {
                Text("Detail View")
            }
            NavigationLink(value: Route.user(id: "tsuzuki817")) {
                Text("User View")
            }
            NavigationLink(value: Route.web(url: URL(string: "https://google.com")!)) {
                Text("Web View")
            }
        }
        .navigationTitle("Home")
        .navigation(path: $appEnvironment.homePath)
    }
}

TabViewのように普通に実装したらnavigationDestinationを各Viewに宣言する必要がありますが、Modifierを使うことで一箇所で管理できています。

AppEnvironmentに全てのNavigationのパスが保持されているのでこの配列をいじるだけでNavigationを制御することができます。

Button {
    appEnvironment.homePath.append(.detail)
} label: {
    Text("Button Navigation")
}

Popさせる方法

今のままの実装だと画面をPOPしようとすると、他のタブと区別がつかずどのpathの配列をいじれば良いのかわかりませんので区別できるようにしていきます。

新しくタブのenumを作ります!

enum SampleTab: Hashable {
    case home
    case search
    case mypage

    var title: String {
        switch self {
        case .home:
            return "Home"
        case .search:
            return "Search"
        case .mypage:
            return "MyPage"
        }
    }
}

またAppEnvironmentに以下を追加します。

class AppEnvironment: ObservableObject {
	@Published var selectedTab: SampleTab = .home

	var tabSelection: Binding<SampleTab> {
        Binding { [weak self] in
            self?.selectedTab ?? .home
        } set: { [weak self] newValue in
            self?.selectedTab = newValue
        }
  }
	.
	.
	.

	func pop(to route: Route) {
        switch selectedTab {
        case .home:
            homePath.removeAll(where: { $0 == route })
        case .search:
            searchPath.removeAll(where: { $0 == route })
        case .mypage:
            myPagePath.removeAll(where: { $0 == route })
        }
    }
}
  • @Published var selectedTab: SampleTab
    • 現在選択しているタブを管理
  • var tabSelection: Binding
    • TabViewが切り替わる時の処理
  • func pop(to route: Route)
    • popするための処理を追加

RootView

TabViewを含めた画面は以下のようになります。

struct RootView: View {
    @EnvironmentObject var appEnvironment: AppEnvironment

    var body: some View {
        TabView(selection: appEnvironment.tabSelection) {
            HomeView()
                .tabItem {
                    Image(systemName: "house")
                    Text("Home")
                }
                .tag(SampleTab.home)
            SearchView()
                .tabItem {
                    Image(systemName: "magnifyingglass")
                    Text("Search")
                }
                .tag(SampleTab.search)
            MyPageView()
                .tabItem {
                    Image(systemName: "person")
                    Text("MyPage")
                }
                .tag(SampleTab.mypage)
        }
    }
}

DetailView

以下のようにappEnvironmentからpopメソッドを呼ぶだけで元の画面に戻ることができます🐔

struct DetailView: View {
    @EnvironmentObject var appEnvironment: AppEnvironment

    var body: some View {
        VStack {
            Image(systemName: "note")
            Text("Detail View")

            Text(appEnvironment.selectedTab.title + "からの遷移")

            Button {
                appEnvironment.pop(to: .detail)
            } label: {
                Text("pop")
            }

        }
        .font(.title)
    }
}

最後に

NavigationView時代に比べてよりNavigationの遷移が制御しやすくなりました!
ぜひ皆さんも新しい画面遷移を体験してみてください!

サンプルリポジトリを作成しましたのでこちらからご覧ください🙇
https://github.com/tsuzukihashi/SampleNavigationStack

最後まで見ていただきありがとうございました!

38
24
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?