StateとObservableを使ってSwiftUIのビューを変更する方法

SwiftUIフレームワークを使っているとStateやObservedObjectと言った機能がよく使われています。特に、勉強を始めた頃などは「この機能が何をしているのか」や「どうやって使えばいいの?」と言った疑問があると思います。今回は、これらの機能についてわかりやすく解説していきます。

SwiftUIとは

今までiOSアプリ開発で使ってきたUIKitよりも、より簡単でインタラクティブにUIを実装できるようにしたフレームワークです。SwiftUIは設計自体が変わっていて、階層化されていてUIの配置がわかりやすくなっていたり、レイアウトなどを調整してくれるようになっていたり、デザインの変更が簡単にできるようにインターフェースがわかりやすくなっています。詳しくは、以下の記事を読んでみてください。

Stateとは

Property Wrappersで実装されていて、SwiftUIが提供しているAttributeの1つです。ビューに@Stateをつけた変数に変更があると、その時点でビューを更新するようになっています。Webのフロントエンドを開発している人は、ReactやVueで使われているStateプロパティに近いものと考えるとわかりやすいと思います。

    @State var isSelected: Bool = false

Property Wrappersとは

State属性はProperty Wrappersという機能で実装されたものと説明しました。では、このProperty Wrappersとはなんでしょうか。この機能はAttributeを宣言する機能で、@を使ってプロパティの更新や取得時に何らかの処理を加えることができます。Stateは、値の更新時にUIの更新をする機能を持っていると言えます。
  • Attributeを宣言する機能で@を使ってアクセスできるようになる
  • Attributeはプロパティにつけるとことで特定の機能を持たせることができる
  • @ を使用することでプロパティへの独自のアクセスを提供する
  • @State @ObservedObject @Published などがあげられる

State属性を実際に使ってみる

State属性は以下のような機能を持っています。

  • @Stateをつけたプロパティに変更があるとその時点でUIを更新する
  • ReactやVueのStateプロパティに近い
  • $をつけることで本来の値にアクセスができる
  • 値はSwiftUIが持つStoreに保持される

この機能を利用して、ビューの色をかえるサンプルを実装してみます。

State属性を使った簡単に作れるサンプル。

ボタンを押すとState属性を付与した変数の値が変わって、ビューに自動的に反映する。UIKitの時は、ボタンにアクションを付与して再レイアウトする実装をする必要があったけど、SwiftUIだとState属性を付与するだけでUIが更新できる。 #SwiftUI #Swift pic.twitter.com/hZv4G7CDMY
— Hiro | Swift (@nagami_hiro) December 25, 2019
    import SwiftUI
    
    struct StateSampleView: View {
        @State var isSelected: Bool = false
    
        var body: some View {
            VStack {
                Button(action: {
                    self.isSelected = !self.isSelected
                }) {
                    Text("Change Color")
                }
    
                Circle().foregroundColor(isSelected ? .blue : .yellow)
            }
        }
    }

ObservedObjectとは

State属性と似たようなAttributeに、ObservedObject属性というものがあります。ObservedObjectは、ObservableObjectプロトコルを適用したクラスを監視することができるようにします。ObservableObjectは、Combineフレームワークの機能の1つです。Combineは、非同期にイベントを処理するためのフレームワークです。ObservableObjectは、ReactiveプログラミングのSubscriberと同じような役割を持ています。以下のようなことができます。

  • 実装したクラスやモデルが持つプロパティを監視する
  • 監視しているプロパティが更新されると変更が通知される
  • ObservableObjectを適用したクラスの一部実装にPublished属性を利用することができる

ユースケースと実装例

今回は、ボタンを押すとサークルの色が変わるサンプルを実装してみます。内容は、Modelの値の変更を監視して変化したらサークルの色を更新というシンプルなものです。

モデルの実装は以下のようになります。変更があった場合に、objectWillChangeという値に通知を送ることでビューに伝達することができます。

    import Foundation
    import Combine
    
    class ObservableModel: ObservableObject {
        var objectWillChange = PassthroughSubject<observablemodel, never="">()
    
        var isSelected: Bool = false {
            didSet {
                self.objectWillChange.send(self)
            }
        }
    }

ビューの実装は以下のようになります。変更があると、ObservedObject属性を付与しているmodelに通知が行き、ビューを更新します。

    import Foundation
    import SwiftUI
    import Combine
    
    struct ObservableSampleView: View {
        @ObservedObject(initialValue: ObservableModel()) var model: ObservableModel
    
        var body: some View {
            VStack {
                Button(action: {
                    self.model.isSelected = !self.model.isSelected
                }) {
                    Text("Change Color")
                }
                Circle().foregroundColor(model.isSelected ? .blue : .yellow)
            }
        }
    }

詳細画面からアイテムにLikeできるサンプル。

Likeすると一覧画面に反映されて、フィルターをかけたときにLikeされているアイテムだけ表示できるようになってる。これは、Observableの機能を使って変更通知している。値をSwiftUI側が監視してるから、反映も共有しやすくていい。#SwiftUI #Swift pic.twitter.com/IZuvo8ygQI— Hiro | Swift (@nagami_hiro) December 10, 2019

(adsbygoogle = window.adsbygoogle || []).push({});

ObservedObjectは初期化する方法が2つある

最後に、ObservedObjectを付与したプロパティの初期化方法を紹介します。

ビューのinitの引数として渡す

1つ目は、ビューの初期化時に引数として渡してあげる方法です。

    struct ObservableSampleView: View {
        @ObservedObject var model: ObservableModel
    }
    
    let view = ObservableSampleView(model: ObservableModel())

ObservableのinitialValueを使う

1つ目は、ビューの初期化時に引数として渡してあげる方法です。

    struct ObservableSampleView: View {
        @ObservedObject(initialValue: ObservableModel()) var model: ObservableModel
    }