【Swift UI/Combine】@Publishedとは?プロパティの監視と使い方

【Swift UI/Combine】@Publishedとは?プロパティの監視と使い方

この記事からわかること

  • SwiftCombineフレームワーク
  • @Published使い方
  • クラスプロパティ監視する方法

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

Swift UIでクラスのプロパティの変更を監視するために使用する@Published属性についてどのようなものなのかまとめていきます。

※ Swift5.9 / iOS17以降からはObservationフレームワークが登場しています。参考にしてください。

@Publishedとは?

公式リファレンス:@Published

@propertyWrapper  struct Published<Value>

@Publishedとは非同期処理やデータバインディングなどための機能を提供するCombineフレームワークに属するプロパティラッパです。Swift UIでは@ObservedObjectなどとセットで使用するイメージが強いですが、もちろん単体でも役割を持っています。

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

@Publishedの役割は指定したプロパティのPublisherを発行することです。PublisherはCombineフレームワークの肝部分であり状態変換(データなど)検知し通知を出す存在です。それをSubscriber(サブスクライバー)が購読することで値の変化時に任意の処理を施したり、状態変化に応じた処理を実行させることができます。

おすすめ記事:【Swift UI】Combineフレームワークの使い方!PublisherとSubscriberの違い

@Publishedは@Stateと同じく変更を検知するとViewの再レンダリングが行われます。また内部的にはwillSetobjectWillChangesendメソッドを呼び出されているため検知のタイミングは値が変更される直前になります。

おすすめ記事:【Swift】プロパティオブザーバとは?willSetとdidSetの使い方

使い方

@Publishedはプロパティラッパなのでクラスのプロパティを宣言時に付与します。クラスにのみ使用できるように制約がかけられているので構造体では使用できません。

class Weather {
    @Published  var temperature: Double
    init(temperature: Double) {
        self.temperature = temperature
    }
}

Publisherとして操作する

@Publishedの公式ドキュメントを見るとPublisherとしての使用例が記述されています。

class Weather {
    @Published  var temperature: Double
    init(temperature: Double) {
        self.temperature = temperature
    }
}

let weather = Weather(temperature: 20)
cancellable = weather.$temperature
    .sink() {
        print ("Temperature now: \($0)")
}
weather.temperature = 25

上記の例ではPublisherとなったプロパティからsinkメソッドを呼び出すことでサブスクライバーを登録しています。これでプロパティの値の変更が観測されたタイミングでサブスクライバーとして登録された任意の処理を実行させることができます。

再描画の範囲

@Published観測しているプロパティの値の変化を検知してUIを再描画させる仕組みがあると説明しました。実際にどこまでのViewが再描画対象になるのか確認してみます。以下のようにnameageそれぞれを更新するためのメソッドを用意して実行してみます。

final class ViewModel: ObservableObject {
    @Published  private(set) var name = "未設定"
    @Published  private(set) var age = 0
    func updateName() { name = "太郎" }
    func updateAge() { age += 1 }
}

struct UserInfoView: View {
    @StateObject  private var viewModel = ViewModel()

    init() {
        print("init UserInfoView")
    }
    
    var body: some View {
        VStack(spacing: 16) {
            InfoView(title: "名前", value: viewModel.name)
            InfoView(title: "年齢", value: "\(viewModel.age)")
            
            Button("名前だけ変更") {
                viewModel.updateName()
            }
            
            Button("年齢だけ変更") {
                viewModel.updateAge()
            }
        }.padding()
    }
}

struct InfoView: View {
    let title: String
    let value: String
    init(
        title: String,
        value: String
    ) {
        print("init \(value)")
        self.title = title
        self.value = value
    }
    
    var body: some View {
        Text("\(title): \(value)")
    }
}

結論から言うと各プロパティのどちらかが変更されるたびにView全体に再描画処理が走ります。また実際の値に変化が起きていなくても再描画処理が走っていることも確認できます。パフォーマンスで見ると少し冗長な再描画処理も走ってしまう仕組みになっていることがわかりました。

init UserInfoView
init 未設定
init 0
// updateName実行
init 太郎
init 0
// updateAge実行
init 太郎
init 1
// updateName実行 (実際の値に変化はないのに再描画される)
init 太郎
init 1

値の変化がない時の冗長な再描画を最適化する

値の変化がない時の冗長な再描画を最適化するためにはiOS17以降から使用できる@Observableマクロを活用することで解決できました。ViewModelの実装を以下のように修正します。

@Observable
final class ViewModel {
    private(set) var name = "未設定"
    private(set) var age = 0
    func updateName() { name = "太郎" }
    func updateAge() { age += 1 }
}

View側も@StateObjectは不要になり@Stateに変更します。

@State  private var viewModel = ViewModel()

これで同じ動作を行ってみると実際の値に変化がない場合は再描画処理が走っていないことを確認できます。

init UserInfoView
init 未設定
init 0
// updateName実行
init 太郎
init 0
// updateAge実行
init 太郎
init 1
// updateName実行 (実際の値に変化はないため再描画されない)

View側で参照していない値の変化での再描画

また@PublishedではView側で参照していない値が変化した場合でも再描画処理が走ってしまいます。例えば以下のようにUserStateに管理を任せてageプロパティはView側からは参照しないようにしたとします。

struct UserState {
    var name: String = "初期ユーザー"
    var age: Int = 20
    mutating func updateName() { name = "太郎" }
    mutating func updateAge() { age += 1 }
}

final class ViewModel: ObservableObject {
    @Published  private(set) var state = UserState()
    func updateName() { state.updateName() }
    func updateAge() { state.updateAge() }
}
InfoView(title: "名前", value: viewModel.state.name)
// ageを参照しないように変更
//InfoView(title: "年齢", value: "\(viewModel.state.age)")

しかしageの値の変化を検知するとViewが再描画されてしまいます。

init 初期ユーザー
// updateName実行
init 太郎
// updateName実行
init 太郎
// updateAge実行 (ageは参照していないが再描画される)
init 太郎

実はこれも@Observableマクロで解決することができます。@ObservableStateに付与することで再描画の頻度を最適化させることが可能です。

@Observable
final class UserState {
    var name: String = "初期ユーザー"
    var age: Int = 20
    func updateName() { name = "太郎" }
    func updateAge() { age += 1 }
}

final class ViewModel {
    private(set) var state = UserState()
    func updateName() { state.updateName() }
    func updateAge() { state.updateAge() }
}
init 初期ユーザー
// updateName実行
init 太郎
// updateAge実行 (ageは参照していないため再描画されない)
// updateName実行 (実際の値に変化はないため再描画されない)

@Publishedの使い方を説明しているうちに@Observableが以下に上位互換かを説明する流れになってしまいましたiOS17以降しか使えないのが惜しいですが、今後はリプレースがマストになってきそうです。

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

Search Box

Sponsor

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index