swiftreactive-cocoareactive-swift

Synchronising combined Properties in ReactiveSwift


I'm considering converting a project using my own custom signal framework to use ReactiveSwift instead, but there is a fundamental issue I've never figured out how to resolve in ReactiveSwift:

As a simplified example, let's say you have two mutable properties:

let a = MutableProperty<Int>(1)
let b = MutableProperty<Int>(2)

Then, we derive a property that combines both to implement our logic:

let c = Property.combineLatest(a, b).map { a, b in
    return a + b
}

Later, we receive some information that causes us to update the values of both a and b at the same time:

a.value = 3
b.value = 4

The problem now is that c will inform its listeners that it has the values 3 -> 5 -> 7. The 5 is entirely spurious and does not represent a valid state, as we never wanted a state where a was equal to 3 and b was equal to 2.

Is there a way around this? A way to suppress updates to a Property while updating all of its dependencies to new states, and only letting an update through once you are done?


Solution

  • combineLatest‘s fundamental purpose is to send a value when either of its upstream inputs send a new value, so I don’t think there’s a way to avoid this issue if you want to use that operator.

    If it’s important that both values update truly simultaneously then consider using a MutableProperty<(Int, Int)> or putting the two values in a struct. If you give a little more context about what you’re actually trying to accomplish then maybe we could give a better answer.

    Pausing Updates

    So I really don't recommend doing something like this, but if you want a general purpose technique for "pausing" updates then you can do it with a global variable indicating whether updates are paused and the filter operator:

    let a = MutableProperty<Int>(1)
    let b = MutableProperty<Int>(2)
    
    var pauseUpdates = false
    
    let c = Property.combineLatest(a, b)
        .filter(initial: (0, 0)) { _ in !pauseUpdates }
        .map { a, b in
            return a + b
        }
    
    func update(newA: Int, newB: Int) {
        pauseUpdates = true
        a.value = newA
        pauseUpdates = false
        b.value = newB
    }
    
    c.producer.startWithValues { c in print(c) }
    
    update(newA: 3, newB: 4)
    

    But there are probably better context-specific solutions for achieving whatever you are trying to achieve.

    Using a sampler to manually trigger updates

    An alternate solution is to use the sample operator to manually choose when to take a value:

    class MyClass {
        let a = MutableProperty<Int>(1)
        let b = MutableProperty<Int>(2)
    
        let c: Property<Int>
    
        private let sampler: Signal<Void, Never>.Observer
    
        init() {
            let (signal, input) = Signal<Void, Never>.pipe()
            sampler = input
    
            let updates = Property.combineLatest(a, b)
                .map { a, b in
                    return a + b
                }
                .producer
                .sample(with: signal)
                .map { $0.0 }
    
            c = Property(initial: a.value + b.value, then: updates)
        }
    
        func update(a: Int, b: Int) {
            self.a.value = a
            self.b.value = b
            sampler.send(value: ())
        }
    }
    
    let x = MyClass()
    x.c.producer.startWithValues { c in print(c) }
    
    x.update(a: 3, b: 4)
    

    Using zip

    If a and b are always going to change together, you can use the zip operator which waits for both inputs to have new values:

    let a = MutableProperty<Int>(1)
    let b = MutableProperty<Int>(2)
    
    let c = Property.zip(a, b).map(+)
    
    c.producer.startWithValues { c in print(c) }
    
    a.value = 3
    b.value = 4
    

    Use zip with methods for each type of update

    class MyClass {
        let a = MutableProperty<Int>(1)
        let b = MutableProperty<Int>(2)
    
        let c: Property<Int>
    
        init() {
            c = Property.zip(a, b).map(+)
        }
    
        func update(a: Int, b: Int) {
            self.a.value = a
            self.b.value = b
        }
    
        func update(a: Int) {
            self.a.value = a
            self.b.value = self.b.value
        }
    
        func update(b: Int) {
            self.a.value = self.a.value
            self.b.value = b
        }
    }
    
    let x = MyClass()
    x.c.producer.startWithValues { c in print(c) }
    
    x.update(a: 5)
    x.update(b: 7)
    x.update(a: 8, b: 8)
    

    Combining the values into one struct

    I thought I would provide an example of this even though you said you didn't want to do it, because MutableProperty has a modify method that makes it less cumbersome than you might think to do atomic updates:

    struct Values {
        var a: Int
        var b: Int
    }
    
    let ab = MutableProperty(Values(a: 1, b: 2))
    
    let c = ab.map { $0.a + $0.b }
    
    c.producer.startWithValues { c in print(c) }
    
    ab.modify { values in
        values.a = 3
        values.b = 4
    }
    

    And you could even have convenience properties for directly accessing a and b even as the ab property is the source of truth:

    let a = ab.map(\.a)
    let b = ab.map(\.b)
    

    Creating a new type of mutable property to wrap the composite property

    You could create a new class conforming to MutablePropertyProtocol to make it more ergonomic to use a struct to hold your values:

    class MutablePropertyWrapper<T, U>: MutablePropertyProtocol {
        typealias Value = U
    
        var value: U {
            get { property.value[keyPath: keyPath] }
            set {
                property.modify { val in
                    var newVal = val
                    newVal[keyPath: self.keyPath] = newValue
                    val = newVal
                }
            }
        }
    
        var lifetime: Lifetime {
            property.lifetime
        }
    
        var producer: SignalProducer<U, Never> {
            property.map(keyPath).producer
        }
    
        var signal: Signal<U, Never> {
            property.map(keyPath).signal
        }
    
        private let property: MutableProperty<T>
        private let keyPath: WritableKeyPath<T, U>
    
        init(_ property: MutableProperty<T>, keyPath: WritableKeyPath<T, U>) {
            self.property = property
            self.keyPath = keyPath
        }
    }
    

    With this, you can create mutable versions of a and b that make it nice and easy to both get and set values:

    struct Values {
        var a: Int
        var b: Int
    }
    
    let ab = MutableProperty(Values(a: 1, b: 2))
    
    let a = MutablePropertyWrapper(ab, keyPath: \.a)
    let b = MutablePropertyWrapper(ab, keyPath: \.b)
    
    let c = ab.map { $0.a + $0.b }
    
    c.producer.startWithValues { c in print(c) }
    
    // Update the values individually, triggering two updates
    a.value = 10
    b.value = 20
    
    // Update both values atomically, triggering a single update
    ab.modify { values in
        values.a = 30
        values.b = 40
    }
    

    If you have the Xcode 11 Beta installed, you can even use the new key path based @dynamicMemberLookup feature to make this more ergonomic:

    @dynamicMemberLookup
    protocol MemberAccessingProperty: MutablePropertyProtocol {
        subscript<U>(dynamicMember keyPath: WritableKeyPath<Value, U>) -> MutablePropertyWrapper<Value, U> { get }
    }
    
    extension MutableProperty: MemberAccessingProperty {
        subscript<U>(dynamicMember keyPath: WritableKeyPath<Value, U>) -> MutablePropertyWrapper<Value, U> {
            return MutablePropertyWrapper(self, keyPath: keyPath)
        }
    }
    

    Now instead of:

    let a = MutablePropertyWrapper(ab, keyPath: \.a)
    let b = MutablePropertyWrapper(ab, keyPath: \.b)
    

    You can write:

    let a = ab.a
    let b = ab.b
    

    Or just set the values directly without creating separate variables:

    ab.a.value = 10
    ab.b.value = 20