よっしーブログ

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

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

こんにちは。iOSエンジニアをしております。よっしーです。
今回はSwiftUIでよく使用する、@ObservedObjectについてサンプルを実装して理解を深めたいと思います。

はじめに

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

yosshi-ios.hatenablog.com

環境

今回使用している各バージョンは下記です。

・[Xcode] 14.1
・[Swift] 5.7.1
・[iOS] 16.1

@ObservedObjectとは

@ObservedObjectを使用すると、複数のプロパティの変更を監視し、更新された時にビューに表示されている値も更新する為のものです。
@Stateを複数扱う場合に使うってことですね!

@Stateなんじゃそらと思った方は前回書いた記事を御覧ください。

yosshi-ios.hatenablog.com

使い方

  • 対象とするクラスに@ObservableObjectプロトコルを準拠させます。
class GameSoft: ObservableObject {
        
}
  • 監視対象の変数を@Publishedで宣言します。
class GameSoft: ObservableObject {
    @Published var name = "ゼ○ダの伝説"
    @Published var price = 7678
}
struct ContentView: View {
    @ObservedObject var gameSoft = GameSoft() // インスタンス化
        
    var body: some View {
    }
}

サンプル

Text二つとボタンが表示されており、「100円の値上げをする」ボタンを押下すると値の更新によって再描画されすぐに画面に表示されている価格が変更されます。

コードは以下です。

class GameSoft: ObservableObject {
    @Published var name = "ゼ○ダの伝説"
    @Published var price = 7678
}

struct ContentView: View {
    @ObservedObject var gameSoft = GameSoft()
        
    var body: some View {
        
        VStack(spacing: 10) {
            Text("ゲームタイトル: \(gameSoft.name)")
            Text("価格: \(gameSoft.price)")
            
            Button {
                gameSoft.price += 100
            } label: {
                Text("100円の値上げをする")
            }
        }
    }
}

丸ごと別のViewに共有する

インスタンスをそのまま別のViewに共有することができます。
以下のサンプル、子Viewでゲームタイトルを変更し親Viewに戻るとインスタンスが共有されているので、変更されているのが分かります。

子Viewでも共有するインスタンスに@ObservedObjectをつけます。
以下がコードです。

class GameSoft: ObservableObject {
    @Published var name = "ゼ○ダの伝説"
    @Published var price = 7678
}

struct ContentView: View {
    @ObservedObject var gameSoft = GameSoft()
        
    var body: some View {
        
        NavigationView {
            VStack(spacing: 15) {
                Text("ゲームタイトル: \(gameSoft.name)")
                
                NavigationLink {
                    // 子Viewにそのままインスタンスを共有
                    SecoundView(gameSoft: gameSoft)
                } label: {
                    Text("タイトル変更")
                }
            }
        }
    }
}

struct SecoundView: View {
    // 共有するインスタンスにもObservedObjectをつける
    @ObservedObject var gameSoft: GameSoft
    
    var body: some View {
        VStack {
            Text("ゲームタイトル: \(gameSoft.name)")
            TextField("ゲームタイトルを入力", text: $gameSoft.name)
                .textFieldStyle(RoundedBorderTextFieldStyle())
        }
    }
}

一部分Viewに共有する

一部分を共有する場合は、子Viewに@Bindingをつけます。
親Viewの方は、$を付けて参照渡しを行います。
サンプルの挙動は変わらないので、コードだけ記載します。

class GameSoft: ObservableObject {
    @Published var name = "ゼ○ダの伝説"
    @Published var price = 7678
}

struct ContentView: View {
    @ObservedObject var gameSoft = GameSoft()
        
    var body: some View {
        
        NavigationView {
            VStack(spacing: 15) {
                Text("ゲームタイトル: \(gameSoft.name)")
                
                NavigationLink {
                    // 子Viewに$をつけて参照渡し
                    SecoundView(gameSoftName: $gameSoft.name)
                } label: {
                    Text("タイトル変更")
                }
            }
        }
    }
}

struct SecoundView: View {
    // @Bindingをつけて一部分を共有
    @Binding var gameSoftName: String
    
    var body: some View {
        VStack {
            Text("ゲームタイトル: \(gameSoftName)")
            TextField("ゲームタイトルを入力", text: $gameSoftName)
                .textFieldStyle(RoundedBorderTextFieldStyle())
        }
    }
}

まとめ

@ObservedObjectとはデータバインディングの仕組みの一つであり、データクラスの更新を監視するもの。
変更を監視するクラスに、@ObservableObjectプロトコルを準拠させ@Published と組み合わせて使用する。

次回は@StateObjectについて書きたいと思っています。
勉強中ですので、間違っている点等ございましたら、教えていただけると助かります。

参考リンク

Apple Developer Documentation

【Swift UI】@ObservedObjectの意味と使い方!クラスとプロトコルとの関係

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

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エンジニアをしております。よっしーです。
SwiftUIは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