Synchronising combined Properties in ReactiveSwift

Viewed 1142

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?

1 Answers

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
Related