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?
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.
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.
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)
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
zip
with methods for each type of updateclass 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)
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)
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