【SwiftUI】NavigationViewが上手く画面遷移しない時の対応

SwiftUI

SwiftUIではNavigationViewでの画面遷移がよく行われます。
この画面遷移はユーザーの操作だけではなくコード側から行う事ができ、
SceneStrageでの状態維持やDeepLinkでの起動時などにも使用します。

この画面遷移動作ですが、基本的にアニメーションを伴う様になっており、
階層が深いものを一度に変えるとアニメーションが追いつかず、
正常に画面遷移しない場合があります。

なお、iOS15で若干改善されたようで、
表示するViewが軽いと上手く遷移する場合があります。

上手く遷移しない例

このように階層になっているNavigationViewでコード側で遷移させる際に
深い階層からルートまで戻って別の深い階層まで行く場合に起きます。

まずざっくりViewのコードの説明です。

struct ContentView: View {
    
    @SceneStorage("Genre") var genre:String?
    @SceneStorage("Item") var item:String?
        
    var body: some View {
        NavigationView{
            List{
                NavigationLink(
                    destination: VegetableView(),
                    tag: "vegetable",
                    selection: self.$genre,
                    label: {Text("vegetable")})
                NavigationLink(
                    destination: FruitsView(),
                    tag: "fruits",
                    selection: self.$genre,
                    label: {Text("fruits")})
            }.navigationTitle("Genre")
        }
    }
}

struct VegetableView: View {
    
    @SceneStorage("Item") var vegetable:String?
    
    let vegetableList = ["tomato","carrots","onion"]
    
    var body: some View{
        List{
            ForEach(self.vegetableList ,id:\.self){ vegetable in
                NavigationLink(
                    destination: Text(vegetable),
                    tag: vegetable,
                    selection: self.$vegetable,
                    label: {Text(vegetable)})
            }
        }.navigationTitle("Vegetable")
    }
}

struct FruitsView: View {
    
    @SceneStorage("Item") var fruits:String?
    
    let fruitsList = ["apple","banana","strawberry"]
    
    var body: some View{
        List{
            ForEach(self.fruitsList ,id:\.self){ fruits in
                NavigationLink(
                    destination: Text(fruits),
                    tag: fruits,
                    selection: self.$fruits,
                    label: {Text(fruits)})
            }
        }.navigationTitle("Fruits")
    }
}

コードで遷移させるため、NavigationLinkの引数にselectionがあるものを使用し、
SceneStorageの変数を使っています。

このViewを以下の様なコードで遷移させます。

self.genre = "fruits"
self.item = "apple"

遷移を実行させるのはButtonでも何でも良いです。

これをvegetable>tomatoの様な反対側の深い階層から行うと上手く画面遷移できない場合があります。
上手く動作す場合は適宜階層を深くするなりwaitを入れるなりして処理を重くしてください。

この画面遷移は、描画済みのViewの場合にアニメーションを伴って遷移する場合があります。
その場合にパラメータの変化にアニメーションが追いついていないと正常に画面遷移が行われません。

対策1

描画済みのViewの場合にアニメーションが行われます。
描画前に遷移先を設定してしまえば問題ありません。

描画済みの場合は一旦消してから遷移先を設定し、再度描画してしまいましょう。

struct ContentView: View {
    
    @SceneStorage("Genre") var genre:String?
    @SceneStorage("Item") var item:String?
    
    @State var disp = true
    var body: some View {
        if disp {
            NavigationView{
                //略
            }
        }
    }
}
//遷移先の設定
disp = false
self.genre = "fruits"
self.item = "apple"

DispatchQueue.main.asyncAfter(deadline: .now() + 0.1){
    disp = true
}

このように一旦消して、消えている間に設定することで、
アニメーションを回避して正常に遷移した状態で描画することができます。

対策2

どうしてもアニメーションして見せたい場合は1画面ずつ遷移させましょう。
onAppearなどで表示後に次の遷移を行うか、DispatchQueueやasyncを使って非同期で
十分なマージンを取って時間差でパラメータを設定しましょう。
なお画面遷移にかかる時間は端末や処理内容で変わるので、時間で指定するのはあまりおすすめできません。

self.genre = nil
self.item = nil

DispatchQueue.main.asyncAfter(deadline: .now() + 1){
    self.genre = "fruits"
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2){
    self.item = "apple"
}

これはDispatchQueueで時間差で遷移する例です。

nilで初期画面に戻すまでは上手く動作してくれるたので、
その後の遷移を順に行うことで無事に遷移させています。

余談

今回のソースはDeepLinkの時のソースから抜粋して使っています。
全文が見たい方はこちらをご覧ください。

【SwiftUI】ローカル通知と通知からのアプリ起動(DeepLink)
SwiftUIアプリでローカル通知を出し、通知から起動後に自動で画面遷移を行います。通知はバックグラウンドとフォアグラウンドに対応、アプリは起動済みにも未起動にも対応しています。 特にアプリ未起動時の通知からの起動は、AppDelegate...

コードでの画面遷移を行う場合は、
通知やウィジェットからDeepLinkで起動する場合も多いと思うので、
参考になると思います。

コメント

タイトルとURLをコピーしました