Setara dengan properti yang dihitung menggunakan @Diterbitkan di Swift Combine?

20

Dalam Swift imperatif, adalah umum untuk menggunakan properti yang dihitung untuk memberikan akses mudah ke data tanpa status duplikasi.

Katakanlah saya membuat kelas ini untuk penggunaan imperatif 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
    }

    // ...
}

Jika saya ingin membuat setara reaktif dengan Combine, misalnya untuk digunakan dengan SwiftUI, saya dapat dengan mudah menambahkan @Publishedke properti yang disimpan untuk menghasilkan Publishers, tetapi tidak untuk properti yang dihitung.

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

Ada berbagai solusi yang dapat saya pikirkan. Sebagai gantinya, saya dapat membuat properti yang dihitung disimpan dan tetap diperbarui.

Opsi 1: Menggunakan pengamat properti:

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

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

    // ...
}

Opsi 2: Menggunakan a Subscriberdi kelas saya sendiri:

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

    // ...
}

Namun, solusi ini tidak seanggun properti yang dihitung. Mereka menduplikasi keadaan dan mereka tidak memperbarui kedua properti secara bersamaan.

Apa yang akan setara setara dengan menambahkan Publisherke properti yang dihitung di Combine?

Rberggreen
sumber
1
Properti Terkomputasi adalah jenis properti yang merupakan properti turunan. Nilai-nilainya tergantung pada nilai-nilai tanggungan. Untuk alasan ini saja, dapat dikatakan bahwa mereka tidak pernah dimaksudkan untuk bertindak seperti seorang ObservableObject. Anda secara inheren berasumsi bahwa suatu ObservableObjectobjek harus dapat memiliki kemampuan bermutasi yang, menurut definisi, bukan kasus untuk Computed Property .
nayem
Apakah Anda menemukan solusi untuk ini? Saya berada dalam situasi yang sama persis, saya ingin menghindari negara dan masih dapat menerbitkan
erotsppa

Jawaban:

2

Bagaimana kalau menggunakan downstream?

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

Dengan cara ini, langganan akan mendapatkan elemen dari hulu, maka Anda bisa menggunakan sinkatau assignmelakukan didSetide.

ytyubox
sumber
2

Buat penerbit baru yang berlangganan properti yang ingin Anda lacak.

@Published var speed: Double = 88

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

Anda kemudian dapat mengamatinya seperti @Publishedproperti Anda .

private var subscriptions = Set<AnyCancellable>()


override func viewDidLoad() {
    super.viewDidLoad()

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

Meskipun tidak terkait langsung tetapi bermanfaat, Anda dapat melacak beberapa properti dengan cara itu 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()
}()
Pomme2Poule
sumber
0

Anda sholud mendeklarasikan PassthroughSubject di ObservableObject Anda:

class ReactiveUserManager1: ObservableObject {

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

    [...]
}

Dan di didSet (willSet bisa lebih baik) dari var @Published Anda, Anda akan menggunakan metode yang disebut 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()
    }

    [...]
}

Anda dapat memeriksanya di WWDC Data Flow Talk

Nicola Lauritano
sumber
Anda harus mengimpor Combine
Nicola Lauritano
Bagaimana hal ini berbeda dari opsi 1 dalam pertanyaan itu sendiri?
nayem
Tidak ada PassthroughSubject di option1
Nicola Lauritano
Ya, sebenarnya bukan itu yang saya minta. @Publishedwrapper dan PassthroughSubjectkeduanya memiliki tujuan yang sama dalam konteks ini. Perhatikan apa yang Anda tulis dan apa yang ingin dicapai oleh OP. Apakah solusi Anda berfungsi sebagai alternatif yang lebih baik daripada opsi 1 sebenarnya?
nayem
0

scan ( : :) Mengubah elemen dari penerbit hulu dengan memberikan elemen saat ini ke penutupan bersama dengan nilai terakhir yang dikembalikan oleh penutupan.

Anda dapat menggunakan pemindaian () untuk mendapatkan nilai terbaru dan saat ini. Contoh:

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

}

Kode di atas setara dengan ini: (kurang Kombinasikan)

  @Published var loading: Bool = false {
            didSet {
                if oldValue == false, loading == true {
                    NotificationCenter.default.post(name: NSNotification.Name("userStateDidChange"), object: nil)
                }
            }
        }
zdravko zdravkin
sumber