Swift Combineで@Publishedを使用して計算されたプロパティと同等ですか?


20

命令型Swiftでは、状態を複製せずにデータへの便利なアクセスを提供するために計算されたプロパティを使用するのが一般的です。

命令型MVCを使用するために作成されたこのクラスがあるとします。

class ImperativeUserManager {
    private(set) var currentUser: User? {
        didSet {
            if oldValue != currentUser {
                NotificationCenter.default.post(name: NSNotification.Name("userStateDidChange"), object: nil)
                // Observers that receive this notification might then check either currentUser or userIsLoggedIn for the latest state
            }
        }
    }

    var userIsLoggedIn: Bool {
        currentUser != nil
    }

    // ...
}

Combineを使用してリアクティブな同等物を作成する場合、たとえばSwiftUIで使用する場合、@Published格納されたプロパティに簡単に追加してを生成できますPublisherが、計算されたプロパティは使用できません。

    @Published var userIsLoggedIn: Bool { // Error: Property wrapper cannot be applied to a computed property
        currentUser != nil
    }

考えられるさまざまな回避策があります。代わりに、計算されたプロパティを保存して、更新し続けることができます。

オプション1:プロパティオブザーバーの使用:

class ReactiveUserManager1: ObservableObject {
    @Published private(set) var currentUser: User? {
        didSet {
            userIsLoggedIn = currentUser != nil
        }
    }

    @Published private(set) var userIsLoggedIn: Bool = false

    // ...
}

オプション2:Subscriber自分のクラスでa を使用する:

class ReactiveUserManager2: ObservableObject {
    @Published private(set) var currentUser: User?
    @Published private(set) var userIsLoggedIn: Bool = false

    private var subscribers = Set<AnyCancellable>()

    init() {
        $currentUser
            .map { $0 != nil }
            .assign(to: \.userIsLoggedIn, on: self)
            .store(in: &subscribers)
    }

    // ...
}

ただし、これらの回避策は、計算されたプロパティほど洗練されていません。それらは状態を複製し、両方のプロパティを同時に更新しません。

PublisherCombineで計算されたプロパティにを追加することと同等の適切なものは何ですか?



1
計算されたプロパティは、派生プロパティであるプロパティの一種です。それらの値は、依存の値に依存します。この理由だけでも、それらがのように振る舞うことを意図したものでは決してないと言えObservableObjectます。あなたは本質的に、ObservableObjectオブジェクトが変更された能力を持つことができるはずであると本質的に仮定します。それは、定義上、計算されたプロパティには当てはまりません。
nayem

これに対する解決策を見つけましたか?私はまったく同じ状況です。状態を回避し、引き続き公開できるようにしたいと思います
erotsppa

回答:


2

ダウンストリームを使用するのはどうですか?

lazy var userIsLoggedInPublisher: AnyPublisher = $currentUser
                                          .map{$0 != nil}
                                          .eraseToAnyPublisher()

この方法で、サブスクリプションは上流から要素を取得し、sinkまたはassignを使用してdidSetアイデアを実行できます。


2

追跡するプロパティにサブスクライブする新しいパブリッシャーを作成します。

@Published var speed: Double = 88

lazy var canTimeTravel: AnyPublisher<Bool,Never> = {
    $speed
        .map({ $0 >= 88 })
        .eraseToAnyPublisher()
}()

その後、あなたの@Published財産と同じようにそれを観察することができます。

private var subscriptions = Set<AnyCancellable>()


override func viewDidLoad() {
    super.viewDidLoad()

    sourceOfTruthObject.$canTimeTravel.sink { [weak self] (canTimeTravel) in
        // Do something…
    })
    .store(in: &subscriptions)
}

直接関連はありませんが、それでも有用ですが、複数のプロパティをで追跡できますcombineLatest

@Published var threshold: Int = 60

@Published var heartData = [Int]()

/** This publisher "observes" both `threshold` and `heartData`
 and derives a value from them.
 It should be updated whenever one of those values changes. */
lazy var status: AnyPublisher<Status,Never> = {
    $threshold
       .combineLatest($heartData)
       .map({ threshold, heartData in
           // Computing a "status" with the two values
           Status.status(heartData: heartData, threshold: threshold)
       })
       .receive(on: DispatchQueue.main)
       .eraseToAnyPublisher()
}()

0

ObservableObjectでPassthroughSubjectを宣言する必要があります。

class ReactiveUserManager1: ObservableObject {

    //The PassthroughSubject provides a convenient way to adapt existing imperative code to the Combine model.
    var objectWillChange = PassthroughSubject<Void,Never>()

    [...]
}

そして、あなたのdidSet(willSetが良いかもしれません)で@published VARあなたが呼び出されるメソッドを使用します送信を()

class ReactiveUserManager1: ObservableObject {

    //The PassthroughSubject provides a convenient way to adapt existing imperative code to the Combine model.
    var objectWillChange = PassthroughSubject<Void,Never>()

    @Published private(set) var currentUser: User? {
    willSet {
        userIsLoggedIn = currentUser != nil
        objectWillChange.send()
    }

    [...]
}

WWDC Data Flow Talkで確認できます。


Combine
Nicola Lauritano

これは質問自体のオプション1とどう違うのですか?
nayem

オプション1にPassthroughSubjectがありません
Nicola

まあ、それは私が実際に尋ねたものではありませんでした。このコンテキストでは、@PublishedラッパーとPassthroughSubjectどちらも同じ目的を果たします。あなたが書いたものと、OPが実際に達成したかったことに注意してください。あなたのソリューションは、実際にはオプション1より優れた代替手段として機能しますか?
nayem

0

scan( :)現在の要素をクロージャに提供することにより、上流パブリッシャからの要素を、クロージャによって返された最後の値とともに変換します。

scan()を使用して、最新の現在の値を取得できます。例:

@Published var loading: Bool = false

init() {
// subscriber connection

 $loading
        .scan(false) { latest, current in
                if latest == false, current == true {
                    NotificationCenter.default.post(name: NSNotification.Name("userStateDidChange"), object: nil) 
        }
                return current
        }
         .sink(receiveValue: { _ in })
         .store(in: &subscriptions)

}

上記のコードはこれと同等です:(結合が少ない)

  @Published var loading: Bool = false {
            didSet {
                if oldValue == false, loading == true {
                    NotificationCenter.default.post(name: NSNotification.Name("userStateDidChange"), object: nil)
                }
            }
        }
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.