『入る学科間違えた高専生』の日記

プログラミングのコードを書いたりする予定です。あとは日記等。あといつまで高専生やねん

SwiftUI で UIKit の popToRootViewController を実現する

はじめに

SwiftUI で NavigationViewcontentNavigationLink による画面遷移をしている場合に特定の何かをした場合に
トップに戻したいという時があったとしたときにどう戻るんだっていうので実際に実装してみたことについてです.

↑今回の想定実装です.

前提

bitbucket.org

今回もサンプルアプリを BitBucket に上げてます.
SafeArea を無視する API を iOS13.0+ でも実行できるものに変更したら実行できるはずです.

UIKit だと?

UIKit だと UINavigationControllerpopToRootViewController(animated:) がありますよね.
これを実行すると popToRootViewController(animated:) で遷移している場合,1番初めの遷移元まで戻れます.これを SwiftUI で実現します.

単純に NavigationLink で実装をしてみる

とりあえず今回は,Life Cycle を SwiftUI App にしてやっています.
プロジェクト名は Sample です.

起動時の画面 2つ目の画面 3つ目の画面

SampleApp.swift

import SwiftUI

@main
struct SampleApp: App {
    var body: some Scene {
        WindowGroup {
            NavigationView {
                ContentView()
            }
        }
    }
}

NavigationView に入れてないと何も出来ないので

ContentView.swift

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Spacer()
            Text("This is 1st view")
                .font(.title)
                .fontWeight(.black)
            Spacer()
            NavigationLink(
                destination: SecondContentView(),
                label: {
                    Text("Go to 2nd view")
                        .foregroundColor(.white)
                        .fontWeight(.medium)
                        .padding(.horizontal, 60)
                        .frame(height: 50)
                        .background(Color.black)
                        .cornerRadius(25)
                })                    
         }
        .navigationBarHidden(true)
    }
}

SecondContentView.swift

struct SecondContentView: View {
    var body: some View {
        ZStack {
            Color.orange
                .ignoresSafeArea()
            VStack {
                Spacer()
                Text("This is 2nd View")
                    .font(.title)
                    .fontWeight(.black)
                    .foregroundColor(.white)
                Spacer()
                NavigationLink(
                    destination: ThirdContentView(),
                    label: {
                        Text("Go to 3rd view")
                            .foregroundColor(.orange)
                            .fontWeight(.medium)
                            .padding(.horizontal, 60)
                            .frame(height: 50)
                            .background(Color.white)
                            .cornerRadius(25)
                    })
            }
        }
        .navigationBarHidden(true)
    }
}

ThirdContentView.swift

struct ThirdContentView: View {
    var body: some View {
        ZStack {
            Color.purple
                .ignoresSafeArea()
            VStack {
                Spacer()
                Text("This is 3rd view")
                    .font(.title)
                    .fontWeight(.black)
                    .foregroundColor(.white)
                Spacer()
                Button(action: {
                    // TODO: popToRoot
                }, label: {
                    Text("Back to top")
                        .foregroundColor(.purple)
                })
                .padding(.horizontal, 60)
                .frame(height: 50)
                .background(Color.white)
                .cornerRadius(25)
            }
        }
        .navigationBarHidden(true)
    }
}

とりあえず3つはこんな感じで実装してみました.

どうやって popToRoot を実現するか

簡単に書くと ContentView

@State var isActive = false

を持たせておいて, NavigationLink の引数の1つである isActive にバインドするという方法です.

ContentView の画面遷移の実装は以下ように変化します.

NavigationLink(
    destination: SecondContentView(),
    isActive: $isActive,
    label: {
        EmptyView()
    })
Button(
    action: { isActive = true },
    label: {
        Text("Go to 2nd view")
            .foregroundColor(.white)
            .fontWeight(.medium)
            .padding(.horizontal, 60)
            .frame(height: 50)
            .background(Color.black)
            .cornerRadius(25)
    })

NavigationLink の label については,そのまま Button を設置しても良いかなと思ったのですが, ネスト深くなりすぎるのあんまり好きじゃないので EmptyView を変わりにおいています.

developer.apple.com

SecondContentView の画面遷移に関しては,今の実装のままでいけます.
ただ,プロパティに

@Binding var isActive: Bool

を追加します. ContentViewSecondContentView の実装の部分にも若干変更が生じます.

SecondContentView(isActive: $isActive)

また, ThirdContentView のプロパティにも SecondContentView と同様に isActive を追加します. Init の実装も変更されるので上記と同じように変更してください.

あとは ThirdContentView の TODO の部分を変更するだけです!!

Button(
    action: { isActive = false },
    label: {
        Text("Back to top")
            .foregroundColor(.purple)
    })

これで冒頭で埋め込んだ動画のような動きができるようになったはずです!

最後に

今回は SwiftUI で UIKit の popToRootViewController の機能を実現する方法を書きました.
ここらへんの機能を Router とか使って実現したいんですけどまだ SwiftUI は人類には早すぎる雰囲気を感じています...