[Swift][Combine] @Published を使う時に気をつけること

     
⌛️ 2 min.
よく使われる @Published の動作を改めて確認してみます。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Sonoma RC
  • Xcode 15 RC
  • iOS 17 RC
  • Swift 5.9

@Published

プロパティ関連する @Publisher を定義する Property Wrapper です。

Apple のドキュメントは、こちら

付与された プロパティが変更される時に、Publish する Property Wrapper です。

気をつけるべき点

@Published を使う時に 気をつけるべき点が 2つあります。

・sink したときに Publish される点
・Publish されるタイミング

よく似ていると思っている CurrentValueSubject と比較しながら見ていきます。

sink したときにも Publish される

@Published が付与された変数が更新されたときに、Publish されることは期待通りです。

ですが、.sink したタイミングで 一度 Publish されることに注意しないといけません。

なお、このタイミングでは その時点での変数の値が使用されて Publish されます。

import Foundation
import Combine

class PublishTimingTest {
    @Published var publishedVar: Int = 0
    var cancellables: Set = Set()
    
    init() {
        $publishedVar
            .sink(receiveValue: { newValue in
                print("publishedVar will have \(newValue)")
                print("publishedVar    has    \(self.publishedVar)")
            })
            .store(in: &cancellables)
    }
}

var test = PublishTimingTest()

// print-out
publishedVar will have 0
publishedVar    has    0

# 上記は、Playground で動作させることができます。

まだ、変数を変更していませんが、.sink したタイミングで一度、Publish されていることがわかります。
このタイミングでは、変数は 0 (初期値として指定した値) を保持していて、newValue にも 0 (初期値として指定した値) が渡されています。

CurrentValueSubject も同じ

この点は、CurrentValueSubject も同じです。

import Foundation
import Combine

class PublishTimingTest {
    @Published var publishedVar: Int = 0
    let currentValue: CurrentValueSubject = CurrentValueSubject(0)
    var cancellables: Set = Set()
    
    init() {
        $publishedVar
            .sink(receiveValue: { newValue in
                print("publishedVar will have \(newValue)")
                print("publishedVar    has    \(self.publishedVar)")
            })
            .store(in: &cancellables)
        currentValue.sink(receiveValue: { newValue in
            print("currentValue will have \(newValue)")
            print("currentValue      has  \(self.currentValue.value)")
        })
        .store(in: &cancellables)
    }
}

var test = PublishTimingTest()

// print-out
publishedVar will have 0
publishedVar    has    0
currentValue will have 0
currentValue      has  0

まだ、変数の値を変更していませんが、@Published 指定された変数を .sink した場合と同じように、CurrentValueSubject を .sink したときにも、Publish されていることがわかります。

Publish されるタイミング

@Published の特徴として気をつけなければいけない点は、@Published は、値が変更される前に Publish される点です。

Apple のドキュメントには、@Published は “willSet block” から publish されると説明されています。

ですので、sink に渡された closure 内で newValue はセットされる値ですが、プロパティ自体は 変更前の値を保持しています。

以下で確認してみます。@Published は、プロパティに付与するので、プロパティの willSet/didSet を使って、その順番を確認しています。

import Foundation
import Combine

class PublishTimingTest {
    @Published var publishedVar: Int = 0 {
        willSet {
            print("publishedVar willSet")
        }
        didSet {
            print("publishedVar didSet")
        }
    }
    var cancellables: Set = Set()
    
    init() {
        $publishedVar
            .sink(receiveValue: { newValue in
                print("publishedVar will have \(newValue)")
                print("publishedVar    has    \(self.publishedVar)")
            })
            .store(in: &cancellables)
    }
}

var test = PublishTimingTest()
test.publishedVar = 1

// print-out
publishedVar will have 0
publishedVar    has    0
publishedVar willSet
publishedVar will have 1
publishedVar    has    0
publishedVar didSet

print-out の最初の2行は、.sink した時に Publish された分です。

その後、publisheVar を変更した時には、”willSet” -> “Publish” -> “didSet” と実行されていることが 読み取れます。
# 上記の試行では、willSet の “後” に Publish されていますが、Apple のドキュメントでは、willSet Block で Publish されるとだけ書いてあります。

CurrentValueSubject の Publish するタイミング

CurrentValueSubject の動作を確認してみます。

CurrentValueSubject の内部プロパティに対しての willSet/didSet は 設定できないので Publish された時の newValue と プロパティ自身の値を確認していきます。

import Foundation
import Combine

class PublishTimingTest {
    @Published var publishedVar: Int = 0 {
        willSet {
            print("publishedVar willSet")
        }
        didSet {
            print("publishedVar didSet")
        }
    }
    let currentValue: CurrentValueSubject = CurrentValueSubject(0)
    var cancellables: Set = Set()
    
    init() {
        $publishedVar
            .sink(receiveValue: { newValue in
                print("publishedVar will have \(newValue)")
                print("publishedVar    has    \(self.publishedVar)")
            })
            .store(in: &cancellables)
        currentValue.sink(receiveValue: { newValue in
            print("currentValue will have \(newValue)")
            print("currentValue      has  \(self.currentValue.value)")
        })
        .store(in: &cancellables)
    }
}

var test = PublishTimingTest()
//test.publishedVar = 1
test.currentValue.value = 2

// print-out
publishedVar will have 0
publishedVar    has    0
currentValue will have 0
currentValue      has  0
currentValue will have 2
currentValue      has  2

// print-out の次の4行は、.sink した時に @Publish と CurrentValueSubject がPublish されたものです。
その次の2行をみると、newValue としては、2が 渡されてきていますが、currentValue にもすでに 2がセットされていることがわかります。

このことから、CurrentValueSubject は、didSet block 相当から Publish していることがわかります。

まとめ

@Published を付与した時に気をつけること2点

@Published を付与した時に気をつけること2点
  • .sink したときに Publish される
  • 変数が変更された時には willSet Block から Publish される
  • .sink したときに Publish されるのは、CurrentValueSubject も同じ
  • CurrentValueSubject は、didSet Block で Publish される

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMatery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。

英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

Swift学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です