見出し画像

SwiftUIのProperty Wrapperをマスターする②〜クラス型につける〜

こんにちは。ママさんエンジニアのトモヨです。
再びProperty Wrapper(プロパティラッパー)について見解を深めていきます。
今回はクラスを子のクラスに引き継がせるときに使用する

@ObservedObject
@StateObject
@EnvironmentObject

の違いを学びます。実際に書いたコードはこちらです。

下記の記事を参考に学びました。とっても詳しく記載されていてわかりやすかったです。私の記事では気になった部分も含め解説していきます。

https://blog.personal-factory.com/2021/01/23/how-to-use-propertywrapper-in-swiftui/

ObservableObjectを準拠させたクラスを用いた参照型のデータオブジェクトの扱い

今まではcounter、textなど値や文字を用いていました。今回はオブジェクトを渡すにはどうやるの?ということです。まず渡すオブジェクトを作ってみましょう。

まずDataSourceというクラスにcounterを持たせてあげましょう

class DataSource {
   var counter = 0
}

ObservableObjectを準拠させます。@Publishedをつけます。これでプロパティがSwiftUIで監視されます。

class DataSourceObservableObject {
  @Published var counter = 0
}

各クラスで値を生成するとき、@ObservedObjectや@StateObjectを付与します。この2つの違いはデータを破棄するときの違いです。

struct SwitchColorViewView {
    @State private var isDanger: Bool = false
    var body: some View {
        VStack {
            Button("Change the Color") {
                isDanger.toggle()
            }
            if isDanger {
                Circle().foregroundColor(.red)
                    .frame(width: 200, height: 200)
            } else {
                Circle().foregroundColor(.green)
                    .frame(width: 200, height: 200)
            }
            StateObjectCounterView()
            ObservedObjcetCounterView()
            Spacer()
        }
    }
}

struct StateObjectCounterViewView {
    @StateObject private var dataSource = DataSource()
    var body: some View {
        VStack {
            Button("increment counter") {
                dataSource.counter += 1
            }
            Text("StateObject count: \(dataSource.counter)")
                .font(.title)
        }
    }
}

struct ObservedObjcetCounterViewView {
    @ObservedObject private var dataSource = DataSource()
    var body: some View {
        VStack {
            Button("increment counter") {
                dataSource.counter += 1
            }
            Text("ObservedObject count: \(dataSource.counter)")
                .font(.title)
        }
    }
}

SwitchColorViewをプレビューしてみましょう。
それぞれのincrement counterをタップし、Change the Colorのボタンを押すとObservedObjectのカウントは0に戻ってしまいます
@StateObjectのデータオブジェクトのライフサイクルはViewが表示してから非表示してからですが、@ObservedObjectはbodyが更新されるまでだそうです。
SwitchColorViewのbodyが更新されたので@ObservedObjectのデータオブジェクトが破棄され、カウントが0に戻ってしまったのですね。
要はこのパターンの場合は@StateObjectを使用しなければなりません。
@ObservedObjectは親Viewが子Viewに渡す時に使用します。

// 親
struct ObservedObjectTestViewView {
    @StateObject private var dataSource = DataSource()
    var body: some View {
        ObservedObjectTestChildView(dataSource: dataSource)
    }
}

// 子
struct ObservedObjectTestChildViewView {
    @ObservedObject var dataSource = DataSource()
    var body: some View {
        VStack {
            Button("increment counter") {
                dataSource.counter += 1
            }
            Text("ObservedObject count: \(dataSource.counter)")
                .font(.title)
        }
    }
}

ここからは私個人の意見です。全部StateObjectでいいじゃん!って感じもしますが、それをしてしまうとずっとdataSourceを保持してしまいます。メモリ逼迫にもなるのでObservedObjectを使った方がいいです。あと、ObservedObjectって明示的に書いてあると、ここがデータの発生元じゃないなってわかります。なのでStateObject、ObservedObjectは使い分けた方がいいと思いました。

@EnvironmentObject

子のクラスにオブジェクトを渡す場合は@ObservedObject、@StateObjectを使用しますが、さらに下の子のクラスに渡す場合はどうなるでしょう


こちらの図のようにChildViewに一回オブジェクトを渡し、ChildViewはこのオブジェクトは知らなくていいはずなのに処理として書かないといけなくなってしまいました。ここで@EnvironmentObjectが登場します。
@EnvironmentObjectを使用すると、親Viewに.environmentObject修飾子をつければ、後は子孫Viewで使いたいViewだけに@EnvironmentObjectを付与することででデータオブジェクトにアクセスすることができます。

//  EnvironmentObjectParentView.swift

import SwiftUI

// 親View
struct EnvironmentObjectParentViewView {
    @StateObject private var dataSource = DataSource()
    var body: some View {
        EnvironmentObjectChildView().environmentObject(dataSource)
    }
}

struct EnvironmentObjectParentView_PreviewsPreviewProvider {
    @StateObject static private var dataSource = DataSource()
    static var previews: some View {
        EnvironmentObjectParentView().environmentObject(dataSource)
    }
}
// EnvironmentObjectChildView.swift

import SwiftUI

// 子view
struct EnvironmentObjectChildViewView {
    var body: some View {
        EnvironmentObjectGrandChildView()
    }
}

struct EnvironmentObjectChildView_PreviewsPreviewProvider {
    static var previews: some View {
        EnvironmentObjectChildView()
            .environmentObject(DataSource())
    }
}
//  EnvironmentObjectGrandChildView.swift

import SwiftUI

// 孫view
struct EnvironmentObjectGrandChildViewView {
    @EnvironmentObject var dataSource: DataSource
    var body: some View {
        Text("\(dataSource.counter)")
    }
}

struct EnvironmentObjectGrandChildView_PreviewsPreviewProvider {
    static var previews: some View {
        EnvironmentObjectGrandChildView().environmentObject(DataSource())
    }
}

子viewのクラスはDataSourceを扱わず、孫のクラスにオブジェクトを渡すことができました。

まとめ

最初は使用用途に苦戦したProperty Wrapperですが、コードを書くことを通して理解を深めることができました。しっかり役割づけもされているのでわかりやすかったです。


この記事が気に入ったらサポートをしてみませんか?