この記事では、SwiftUIでのNavigationView画面とTabView画面間の遷移方法について解説します。
ログイン認証機能を実装しているアプリの場合、ユーザー登録やログインなどの認証機能をNavigationViewを利用して関係画面に遷移、ログイン後はTabViewで関係画面を遷移する方法を取ることが多いと思います。
◆動作検証環境
・XCode:12.1
・SwiftUI:2.0
NavigationViewとTabViewの画面遷移の実装
サンプルアプリの仕様
今回サンプルとして作成するアプリの仕様は以下のとおりです。
認証機能に使用する以下3つのビューをNavigationViewで遷移するように作成します。
- ユーザー登録画面
機能:ログイン画面への遷移 - ログイン画面
機能:ユーザー登録画面への遷移
ログイン処理(ホーム画面へ遷移)
パスワードリセット画面への遷移 - パスワードリセット画面
機能:ログイン画面への遷移 
ログイン完了後に使用する以下2つのビューはTabViewで遷移するように作成します。
- ホーム画面
 - ログイアウト画面
機能:ログアウト処理(ログイン画面(NavigationView)に遷移) 
サンプルアプリのコーディング
AppState.swift
1 2 3 4 5 6 7 8 9 10  | import Foundation class AppState: ObservableObject {     @Published var isNavigateToLoginView = false     @Published var isNavigateToPasswordResetView = false     @Published var isLogin = false }  | 
上記コーディングポイント
5,6行目:
NavigationLinkで仕様するアプリ共通のフラグを定義します。
8行目:
ログイン状態を判断するアプリ共通のフラグを定義します。
SignUpView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38  | import SwiftUI struct SignUpView: View {     @EnvironmentObject var appState: AppState     var body: some View {         NavigationView{             VStack{                 Text("ユーザー登録画面")                     .padding()                 Button(action: {                     self.appState.isNavigateToLoginView = true                 }) {                     Text("ログイン画面へ")                 }                 NavigationLink(destination: LoginView(),                                isActive: $appState.isNavigateToLoginView){                     EmptyView()                 }                 NavigationLink(destination: PasswordResetView(),                                isActive: $appState.isNavigateToPasswordResetView){                     EmptyView()                 }             }         }     } } struct SignUpView_Previews: PreviewProvider {     static var previews: some View {         SignUpView()     } }  | 
上記コーディングポイント
9行目:
NavigationViewの親クラス(ログイン画面、パスワードリセット画面の)として定義
LoginView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31  | import SwiftUI struct LoginView: View {     @EnvironmentObject var appState: AppState     var body: some View {         VStack{             Text("ログイン画面")                 .padding()             Button(action: {                 self.appState.isLogin = true             }) {                 Text("ログインする")                     .padding()             }             Button(action: {                 self.appState.isNavigateToPasswordResetView = true             }) {                 Text("パスワードリセット")             }         }     } } struct LoginView_Previews: PreviewProvider {     static var previews: some View {         LoginView()     } }  | 
上記コーディングポイント
12行目:
ログイン処理が成功し、ホーム画面に遷移する方法として@EnvironmentObjectで扱うアプリ共通のフラグを利用する。
値が変化(false→true)する事で、後述するRootViewが再描画される形となり、TabView画面へ遷移する。
*ここでNavigationViewを使い、ログイン画面→ホーム画面とすると遷移はするが、その後の動きがおかしくなる(NavigationViewの中にさらにNavigationViewが作られるなど)
PasswordResetView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17  | import SwiftUI struct PasswordResetView: View {     @EnvironmentObject var appState: AppState     var body: some View {         Text("パスワードリセット画面")     } } struct PasswordResetView_Previews: PreviewProvider {     static var previews: some View {         PasswordResetView()     } }  | 
ContentView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25  | import SwiftUI struct ContentView: View {     var body: some View {         TabView{             HomeView()                 .tabItem{                     Text("HOME")                 }             LogoutView()                 .tabItem{                     Text("LOGOUT")                 }                         }     } } struct ContentView_Previews: PreviewProvider {     static var previews: some View {         ContentView()     } }  | 
TabView設定のクラス
HomeView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15  | import SwiftUI struct HomeView: View {     var body: some View {         Text("ホーム画面")     } } struct HomeView_Previews: PreviewProvider {     static var previews: some View {         HomeView()     } }  | 
LogoutView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28  | import SwiftUI struct LogoutView: View {     @EnvironmentObject var appState: AppState     var body: some View {         VStack{             Text("ログアウト画面")                 .padding()             Button(action: {                 self.appState.isLogin = false             }) {                 Text("ログアウトする")                     .padding()             }         }     } } struct LogoutView_Previews: PreviewProvider {     static var previews: some View {         LogoutView()     } }  | 
上記コーディングポイント
14行目:
ログイン処理と同様にフラグを変化させることで、ログアウト後の遷移を行う。
*ここでNavigationViewを使うと遷移はするが、その後の動きがおかしくなる(NavigationViewの中にさらにNavigationViewが作られるなど)
RootView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20  | import SwiftUI struct RootView: View {     @EnvironmentObject var appState: AppState     var body: some View {         if appState.isLogin{             ContentView()         }else{             SignUpView()         }     } } struct RootView_Previews: PreviewProvider {     static var previews: some View {         RootView()     } }  | 
上記コーディングポイント
7〜10行目:
アプリ共通のログイン状態を管理するフラグで、アプリ起動時に遷移させる画面をコントロールする。
条件として使うフラグは、@EnvironmentObjectで扱うアプリ共通となるため、変化する度にif文以下の条件分岐が行われる
アクティブ⇔バックグラウンドの遷移であれば、view表示に使用する@EnvironmentObjectのフラグのデータは保持されますが、アプリを再起動した際はデータが消えます。
そのため、再起動前後でUserDefaultsを利用してデバイスにログイン状態を保持し、次回のアプリ立ち上げ時に備えます。
上記のコード内容では、RootViewが呼ばれる時には、UserDefaultsから@EnvironmentObjectのフラグのデータに値が渡されている必要があるため、NSObjectProtocol を利用する方法などを利用します。
詳しい方法はこちらの記事を参考にしてください。
SwiftUIでButtonなどのアクションを利用せずにUserDefaultsで値を制御する
SceneDelegate.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25  | import UIKit import SwiftUI class SceneDelegate: UIResponder, UIWindowSceneDelegate {     var window: UIWindow?     @ObservedObject var appState = AppState()     func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {         let contentView = RootView().environmentObject(appState)         if let windowScene = scene as? UIWindowScene {             let window = UIWindow(windowScene: windowScene)             window.rootViewController = UIHostingController(rootView: contentView)             self.window = window             window.makeKeyAndVisible()         }     }    ...    ...  | 
上記コーディングポイント
9、13行目
RootView.swiftをウェルカムページにするため、SceneDelegate.swiftを以下のように編集します。
以上、SwiftUIでのNavigationView画面とTabView画面間の遷移方法について解説しました。