Most of our projects at all about apps are build using a MVVM architecture with ReactiveSwift as our reactive framework of choice. However, all code examples should be easily translatable to RxSwift.
Often, you end up with an array of objects (e.g. View Models) which contain some reactive property that you want to bind to. The array itself is a MutableProperty
as well and can change at any time (e.g. after a network request).
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class ViewModel {
let value: MutableProperty<String>
init(initialValue: String) {
value = MutableProperty(initialValue)
}
func update(_ value: String) {
self.value.value = value
}
}
let viewModels = MutableProperty<[ViewModel]>([])
|
Usually, you want to react to changes of the view model array and to changes within the view models itself. One way to accomplish this, is to create new bindings whenever the array changes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
var disposable = CompositeDisposable()
viewModels.producer.startWithValues { (viewModels) in
let valueProperties = viewModels.map { $0.value }
disposable.dispose()
disposable = CompositeDisposable()
for property in valueProperties {
disposable += property.signal.observeValues { (value) in
print("new value: \(value)")
}
}
}
|
But this isn’t a very elegant solution and we have to make sure to dispose the existing bindings whenever we create new ones.
A better solution is to use our old friend, flatMap. flatMap(.latest)
can be used to chain network requests:
1
2
3
4
5
6
7
8
|
// login returns a SignalProducer<User, APIError>
APIClient.login(user: "test", password: "test")
.flatMap(.latest) { (user) -> SignalProduder<UserProfile, APIError> in
return APIClient.fetchProfile(user: user)
}
.startWithValue { (profile) in
// do something with the user profile
}
|
But you can also use flatMap
to create a Signal
or SignalProducer
that is dependend on both, the mutable array and the mutable properties within the array without having to re-create the binding whenever the array changes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
lazy var valueChangedSignal: Signal<String, NoError> = {
return self.viewModels.signal.flatMap(.latest) { (viewModels) in
return SignalProducer(viewModels.map { $0.value.signal }).flatten(.merge)
}
}()
// this will fire whenever the value of a view model changes
valueChangedSignal.observeValues { (value) in
print("[valueChangedSignal] value: \(value)")
}
-> set new array: [a, b]
-> update value: a -> x
[valueChangedSignal] value: x
new value: x
|
As you can observe above, the Signal
will emit a new value whenever the MutableProperty
of the view model changes. If we update the whole array though, the Signal
will not emit anything. This can be changed by using a SignalProducer
instead:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
lazy var valueChangedProducer: SignalProducer<String, NoError> = {
return self.viewModels.producer.flatMap(.latest) { (viewModels) in
return SignalProducer(viewModels.map { $0.value.producer }).flatten(.merge)
}
}()
// this will fire whenever the array of view models changes (forwarding all inner values)
// or the value of one of the view models is updated
valueChangedProducer.startWithValues { (value) in
print("[valueChangedProducer] value: \(value)")
}
-> set new array: [a, b]
[valueChangedProducer] value: a
[valueChangedProducer] value: b
-> update value: a -> x
[valueChangedProducer] value: x
new value: x
|
Now, values are emitted whenever the viewModels
array changes as well. However, using flatten(.merge)
will result in emitted signals for every value within the array when we update the array. This could result in multiple unnecessary updates to our view. Instead of using flatten(.merge)
we can use combineLatest
to create our SignalProducer
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
lazy var valueChangedCombined: SignalProducer<[String], NoError> = {
return self.viewModels.producer.flatMap(.latest) { (viewModels) in
return SignalProducer.combineLatest(viewModels.map { $0.value.producer })
}
}()
/// this will fire whenever the array of view models changes or the value of
// one of the view models is updated, always forwarding the all current values
valueChangedCombined.startWithValues { (values) in
let valuesString = values.joined(separator: ", ")
print("[valueChangedCombined] values: \(valuesString)")
}
-> set new array: [a, b]
[valueChangedCombined] values: a, b
-> update value: a -> x
[valueChangedCombined] values: x, b
|
Now, instead of emitting the individual values, we SignalProducer
emits an array of all current values. Whenever the array changes as a whole, we get the one new value, the new array. Whenever a value within the array changes, we get the array with updated values.
Great! We have seen how flatMap
can be used to create a dependent Signal
or SignalProducer
and demonstrated different operators to combine the inner reactive primitives. Details on more operators can be found in the ReactiveSwift documentation.