2

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?

Dag Ågren
  • 1,064
  • 7
  • 18

1 Answers1

4

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
jjoelson
  • 5,771
  • 5
  • 31
  • 51
  • I have run into this problem in many different contexts. Bundling values together is not really possible in many situations without creating very awkward code, too. What I want to do is to combine two different properties, and do atomic updates depending on whatever is happening in the world around it, without triggering spurious updates when I know several things will update in rapid succession. I'd be happy using some other operator, but I don't think anything else exists. – Dag Ågren Jun 27 '19 at 13:21
  • @DagÅgren I updated my answer. But if you can give me one or two concrete examples of where this problem arises I can probably give a more idiomatic solution. If you're running into this issue often, you are probably fighting the framework a bit too much in terms of how your code is structured. – jjoelson Jun 27 '19 at 13:49
  • Using a global flag seems pretty clunky. It should be possible to implement this as some kind of operator, you'd think. – Dag Ågren Jun 27 '19 at 14:11
  • I added a few other options. But like I said, for any given specific example there's probably a better specific answer. – jjoelson Jun 27 '19 at 14:13
  • Added a few more. FWIW, the reason why this is ugly is because `ReactiveSwift` is fundamentally designed for declaratively creating flows of data, and it's difficult to express "do atomic updates depending on whatever is happening in the world around it" declaratively in a general way, though specific cases are likely to have nicer solutions. – jjoelson Jun 27 '19 at 14:32
  • @DagÅgren ok, I kind of went down a rabbit hole and implemented an entire system for wrapping properties (see my latest edit). Hopefully some of this is useful, or at least interesting :-) – jjoelson Jun 27 '19 at 19:52