よっしーブログ

iOSエンジニアやってます。SwiftUI勉強中です。

if letでNavigationLink遷移時アニメーションが無くなる

こんにちは。iOSエンジニアをしております。よっしーです。
SwiftUIについて勉強中です、個人アプリリリース目標で頑張っています。

はじめに

今回はNavigationLink遷移時にアニメーションが無くなった事象と解決内容について書きます。

環境

・[Xcode] 13.0
・[Swift] 5.5
・[iOS] 15.0

事象

リストから選択した情報を二つ目の画面へ遷移時に渡し表示するという簡単なアプリになっています。
画面遷移をする際にアニメーションをして遷移するのですが、
アニメーションがされないという問題がありました。

原因

原因は、if letでNavigationLinkを生成した直後に遷移を発火していることが問題です。以下がソースです。

struct ContentView: View {
    
    let fruits = ["りんご", "みかん", "もも", "バナナ"]
        
    @State var selectedFruit: String?
    @State var isActive = false
    
    var body: some View {

        NavigationView {
            VStack {

                // ここのif letでNavigationLinkを生成した直後に遷移を発火しているのが問題
                if let selectedFruit = self.selectedFruit {
                    NavigationLink(isActive: $isActive) {
                        SecoundView(fruit: selectedFruit)
                    } label: {
                        EmptyView()
                    }
                }
                
                List(fruits, id: \.self) { fruit in
                    HStack {
                        Text(fruit)
                        Spacer()
                    }
                    .contentShape(Rectangle())
                    .onTapGesture {
                        self.selectedFruit = fruit
                        self.isActive = true
                    }
                }
            }
        }
    }
}

struct SecoundView: View {
    let fruit: String
    
    var body: some View {
        Text("選択したフルーツ: \(fruit)")
            .font(.title)
    }
}

解決

もちろん強制アンラップはダメでした。Viewを生成した直後は、nullになるからです。
destinationの中でif letを使用することで解決しました。

NavigationLink(isActive: $isActive) {
    // destinationの中でif letを使用する
    if let selectedFruit = self.selectedFruit {
        SecoundView(fruit: selectedFruit)
    }
} label: {
    EmptyView()
}

アニメーションできるようになりました。よかった〜。

まとめ

NavigationLinkのif letで遷移する場合は、destinationの中に記載する。

間違っている点等ございましたら、教えていただけると助かります。

参考リンク

iOSDC2022で書いてくだった方感謝です。
SwiftUIのハマりどころとその回避策/ iOSDC_otsuka - Speaker Deck

@Bindingについてサンプルで理解する

こんにちは。iOSエンジニアをしております。よっしーです。
SwifUIはUIkitと違って苦戦しています。。
勉強した内容をまとめていきたいと思っています。

はじめに

今回も引き続きSwiftUIのpropertyWrapper編です。
前回は@Stateについて書きました。

yosshi-ios.hatenablog.com

@Bindingもとても重要なので、理解できるようにサンプルを用いて説明しようと思います。

@Bindingとは

@Bindingを付与すると、別のビュー同士の変数を紐づけることができる。親のプロパティを子のViewで変えたい時に使用する。

サンプル実装

担当者チェックのボタンを押下すると状態によって画像が切り替わるという簡単なアプリです。

BindinglsCheckedView(子View)

BindinglsCheckedViewはisCheckedプロパティを保持していてボタン押下時に画像と色を切り替えます。
親から渡されるプロパティを紐付けしたい為@Bindingで宣言します。

struct BindinglsCheckedView: View {
    
    /// 親から渡されるプロパティ
    @Binding var isChecked: Bool
    
    var body: some View {
        
        Button(action: {
            // ボタンを押したらプロパティ更新
            isChecked.toggle()
        }) {
            // isCheckedによって画像を変える
            // isCheckedによってforegroundColorを変える
            Image(systemName: isChecked ? "person.fill.checkmark" : "person")
                .foregroundColor(isChecked ? .blue : .gray)
                .scaleEffect(2.0)
        }
    }
}
ContentView(親View)

親ビュー内に@StateとしてisCheckedPersonがfalseとして定義されています。
BindingでisCheckedPersonをBindinglsCheckedView(子View)
に渡しています。渡すときに、$をつけて渡します。

struct ContentView: View {
    
    @State var isCheckedPerson = false
    
    var body: some View {
                
        HStack {
            Text("担当者チェック")
            
            // 子のビューの呼び出しと紐付けるプロパティを渡す
            BindinglsCheckedView(isChecked: $isCheckedPerson)
        }
    }
}

応用

担当チェックのボタンをもう一つ用意し、両方押下したら
「全員チェック済み」のTextを表示します。
それ以外の場合は「チェック待ち」のTextを表示します。

ContentViewにisCheckedPerson1の他に、isCheckedPerson2を用意しそれを新たに用意したBindinglsCheckedView(子View)に渡し紐付けを行います。
紐付けを行ったプロパティ二つを監視し、状態によってTextの表示を変えます。以下が全ソースです。

/// 親View
struct ContentView: View {
    
    @State var isCheckedPerson1 = false
    @State var isCheckedPerson2 = false
    
    var body: some View {
                
        VStack {
        
            HStack {
                Text("担当者1のチェック")
                
                // 子のビューの呼び出しと紐付けるプロパティを渡す
                BindinglsCheckedView(isChecked: $isCheckedPerson1)
            }
            
            HStack {
                Text("担当者2のチェック")
                
                // 子のビューの呼び出しと紐付けるプロパティを渡す
                BindinglsCheckedView(isChecked: $isCheckedPerson2)
            }
            
            // 子と紐付けた親プロパティを監視
            if isCheckedPerson1 && isCheckedPerson2 {
                Text("全員チェック済み").foregroundColor(.blue)
            } else {
                Text("チェック待ち").foregroundColor(.red)
            }
            
        }
    }
}

/// 子View
struct BindinglsCheckedView: View {
    
    /// 親から渡されるプロパティ
    @Binding var isChecked: Bool
    
    var body: some View {
        
        Button(action: {
            // ボタンを押したらプロパティ更新
            isChecked.toggle()
        }) {
            // isCheckedによって画像を変える
            // isCheckedによってforegroundColorを変える
            Image(systemName: isChecked ? "person.fill.checkmark" : "person")
                .foregroundColor(isChecked ? .blue : .gray)
                .scaleEffect(2.0)
                .frame(width: 50, height: 50)
        }
    }
}

まとめ

@Bindingを付与すると別のビュー同士の変数の紐づけを行うことが出来る。親Viewからは渡すときには$をつけて渡す

次回は@ObservedObjectを書きたいと思っています。
間違っている点等ございましたら、教えていただけると助かります。

参考リンク

Apple Developer Documentation

【SwiftUI】@Bindingの使い方を徹底解説 | iOS-Docs

【Swift UI】@Bindingの意味と使い方とは?親と子の構造体

@Stateについてサンプルで理解する。

はじめに

swiftUIを学習していて、何だこれってなった@Stateについて自分なりにサンプルを実装して理解を深めたいと思います。

@Stateとは

プロパティの値とUIの状態を自動的に同期する仕組みである。 @Stateをつけると二つのことができる。

  • つけたプロパティを監視して更新されたら、Viewが再描画される。
  • 構造体の中に定義したプロパティは内部から変更することはできないが、変更することができるようになる。

サンプル実装

以下のサンプルがボタンを押下すると、画面に表示されているテキストがみかんからりんごに変わるというとても簡単なものです。

@Stateをつけずに確認

変数fruitをみかんで初期化しています。ボタンを押下するとfruitにりんごを代入します。ですが代入箇所でエラーが出ているのが分かります。structに定義された、変数fruitを変更することはできません。mutatingを使用すれば変更することは可能ですが、今回はfruitが変更されたらテキストの表示も変更する必要があるためNGです。

Cannot assign to property: 'self' is immutable

@Stateを追加して確認

変数fruitに@Stateを追加して、プロパティ内部から変更することができるようになった為エラーが消えました。

以下がソースになります。

struct ContentView: View {
    
    @State var fruit = "みかん"
    
    var body: some View {
        
        VStack {
            Text(fruit)
                .font(.title)
                .padding(.bottom, 10)
            
            Button(action: {
                self.fruit = "りんご"
            }) {
                Text("りんごに変えるButton")
            }
        }
    }
}

まとめ

@Statetとは、プロパティの宣言時に使えるSwiftUIのカスタム属性で、プロパティの値とUIの状態を自動的に同期する仕組みを実現するために必要。

参考リンク

Apple Developer Documentation

【SwiftUI】@Stateの使い方 | カピ通信

【SwiftUI】@Stateの使い方を徹底解説 | iOS-Docs