Lento con forza

大学生気分のIT系エンジニアが色々書いてく何か。ブログ名決めました。

iOS 13でもStateObjectが使いたい!

iOS 14から使えるStateObject、とても便利ですよね

developer.apple.com

ObservableObjectなどを初期化するときに便利です。

class Object: ObservableObject {
    @Published
    var value = "value"
}

struct Content: View {
    @StateObject
    var object = Object()

    var body: some View {
        Text(object.value)
    }
}

ただ、これはiOS 14以降でしか使えないため、iOS 13に対応が必要な場合は使えません。代わりにObservedObjectを使います。

struct Content: View {
    @ObservedObject
    var object = Object()

    var body: some View {
        Text(object.value)
    }
}

ただ、これはStateObjectと違い、Contentが再生成されるたびにObjectが再生成されてしまうため、期待通りの動作をしない場合があります。そのため、Objectを外部から与える必要があります。

struct Content: View {
    @ObservedObject
    var object: Object // どうやって初期化する?

    var body: some View {
        Text(object.value)
    }
}

必要なオブジェクトが単一の場合はViewを生成する時に与えることで解決できますが、Listの各要素でそれぞれ必要な場合はどうすれば良いでしょうか。例えばNukeのFetchImageでは、画像1枚ごとにオブジェクトを初期化する必要があります。 そういった時に@StateObjectが便利なのですが、iOS 13では使えないため、@Stateと@ObservedObjectを別々に使うことで解決できます。

struct StateWrapper: View {
    @State
    var object = Object()

    var body: some View {
        Content(object: object)
    }
}

struct Content: View {
    @ObservedObject
    var object: Object

    var body: some View {
        Text(object.value)
    }
}

StateWrapperでラップして、そこでは@Stateをストレージとして使いオブジェクトを保持しています。そのオブジェクトをbodyのObservedObjectを与えます。StateWrapperを挟むことで、それぞれのビューでオブジェクトを生成し、ビューの状態が変わった時も@Stateでオブジェクトが維持され、@StateObjectと同じ効果を得られます。

本来@Stateは値型に対して使うと、オブジェクトのメモリ管理と変更監視を行ってくれますが、参照型に使うと変更監視が使えませんが、メモリ管理はSwiftUIで行ってくれるようになります。そのため、Viewが再生成された時も同じインスタンスを参照するため、 @ObservedObjectと組み合わせることで @StateObject と同じ挙動を再現できます。

一旦これで解決できましたが、他に良い方法があれば教えてください!