web-dev-qa-db-ja.com

Swift Combine?

命令型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を格納されたプロパティに変換してPublishersを生成しますが、計算されたプロパティは対象外です。

    @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を使用する:

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)
    }

    // ...
}

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

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

19
rberggreen

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

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

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

1
ytyubox

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>()

    [...]
}

そして、あなたの@ Published varのdidSet(willSetの方が良いかもしれません)では、send( )

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 で確認できます。

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)
                }
            }
        }
0