[SwiftUI][Swift][Concurrency] MVVM と Swift Concurrency を組み合わせる(3: actor の変更を subscribe する)

     
actor 中の Published された変数の Publisher を使用して変更監視をしてみます。

環境&対象

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

  • macOS Monterey 12.3 beta4
  • Xcode 13.2.1
  • iOS 15.2

これまでの記事は、以下です。
[SwiftUI][Swift] MVVM と Swift Concurrency を組み合わせる(2: View/ViewModel から actor にアクセスする) SwiftUI2021[SwiftUI][Swift] MVVM と Swift Concurrency を組み合わせる(1: Model を actor に)

Actor の変数を監視

前回では actor の値を取得して、ViewModel の 変数を更新するために メソッドを作成して行っていました。

大抵のケースでは、変数が更新されるタイミングは明確なので、更新のたびにメソッドを呼び出しても良いのですが、せっかくなので、Combine をつかって更新処理をしてみます。

actor の変数を @Published に

Combine で仕組みとして用意されている @Published を使用します。

今回のケースでは、Model(actor Countdown) の count プロパティを @Published 指定して Publisher として使用します。

前回 作成した actor を以下のように修正します。(count に @Published 指定を付与しています)


//
//  Countdown.swift
//
//  Created by : Tomoaki Yagishita on 2022/03/01
//  © 2022  SmallDeskSoftware
//

import Foundation

actor Countdown {
    @Published var count: Int = 0
    
    init(_ count: Int) {
        precondition(count >= 0)
        self.count = count
    }
    
    func decrement() {
        if count == 0 { return }
        count -= 1
    }
    func increment() {
        count += 1
    }
}

前回からの相違点は、以下の通りです。
・count に @Published を付与した。

@Published は、付与された変数の Publisher を用意する Property Wrapper なので 実は View に直接関係しないところでも便利に使えます。

以下の記事で説明してます。
[SwiftUI][Combine] @Published は、Publisher を提供する Property Wrapper

Publisher を Subscribe する

@Published を付与したことで、Publisher を使うことができるようになりましたので、ViewModel 側で Subscribe して変更された時に更新するようにします。

actor であることを意識せずに書くと以下のようになります。(Countdown が class であれば普通に使えます)

ViewModel の init 周辺だけ抜粋

class ViewModel: ObservableObject {
    let countdown = Countdown(60)
    @Published var count: Int
    
    var cancellables: Set = Set()
    
    init() {
        count = 0
        countdown.$count
            .sink{ newValue in
                print("newValue \(newValue)")
            }
            .store(in: &cancellables)
    }
// snip 
    

上記のコードでは以下のコンパイルエラーになります。


actor-isolated property '$count' can not be referenced from a non-isolated context

actor が保有するプロパティは、Publisher であっても、自由にアクセスできないルールは同じです。

ですので、async アクセスが必要となります。

呼ぼうとしている init は、sync なメソッドです。そのままでは async なアクセスはできないので、Task を作成してアクセスする必要があります。
ということで、以下のようになります。

ViewModel の init だけ抜粋

    init() {
        count = 0
        Task {
            await countdown.$count
                .sink{ newValue in
                    print("newValue \(newValue)")
                }
                .store(in: &cancellables)
        }
    }

動作させてみると確認できますが、値が更新されるたびに、"newValue ~数字~" と Xcode のコンソールに表示されます。

以前は、decrement の処理後に、ViewModel 内から updateAsync を呼び出して ViewModel の持つ count を更新していましたが、この sink 内で更新するようにすれば、decrement 内で updateAsync を明示的に呼び出す必要はなくなります。

改良した init

    init() {
        count = 0
        Task {
            await countdown.$count
                .sink{ newValue in
                    Task {
                        await self.updateAsync()
                    }
                }
                .store(in: &cancellables)
        }
    }

.sink の中は sync なので、async なメソッドを呼び出すには、Task を生成する必要があります。

まとめ

actor からも @Published で Publisher を公開できることを確認しました。

actor からも @Published で Publisher を公開できる
  • 普通に、@Published と付与すれば、Publisher を作成できる
  • ただし .sink するためには、async アクセスが必要
  • .sink で動作させるコードの context (thread) に注意が必要なケースもある

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

コメントを残す

メールアドレスが公開されることはありません。