The key here is to look at each effect as an individual thing and define what causes that effect.
Let's assume that you have methods for getting and saving user data. getUserData() -> Observable<UserData> (this get will emit one value and then complete) and save(_ userData: UserData). Further, for simplicity, I'll assume that these can't error.
So let's look at each effect in turn.
- Filling out the initial value for firstName:
getUserData()
.map(\.firstName)
.bind(to: firstName.rx.text)
.disposed(by: disposeBag)
Well that was simple.
- Filling out the initial value for lastName. This is a bit tougher. We don't want to subscribe twice to
getUserData() because that would cause two network requests... The share() operator helps here:
let userData = getUserData()
.share()
userData
.map(\.firstName)
.bind(to: firstName.rx.text)
.disposed(by: disposeBag)
userData
.map(\.lastName)
.bind(to: lastName.rx.text)
.disposed(by: disposeBag)
or if you want to compress it a bit:
let userData = getUserData()
.share()
disposeBag.insert(
userData.map(\.firstName).bind(to: firstName.rx.text),
userData.map(\.lastName).bind(to: lastName.rx.text)
)
- The email is the same:
let userData = getUserData()
.share()
disposeBag.insert(
userData.map(\.firstName).bind(to: firstName.rx.text),
userData.map(\.lastName).bind(to: lastName.rx.text),
userData.map(\.email).bind(to: email.rx.text)
)
- The next effect, the
save sounds like the one you are having trouble with. I have to make some more assumptions here, but I'm going to guess that you need to send a fully formed, correct, user data object. If the user just changes their first name, you still have to send the old last name and email address with the new first name in the user data object.
So where do you get the original data from? That's pretty obvious I hope, the userData Observable above. Also, you need to collect anything the user types in any of the three fields.
Here's the twist... When you subscribe to a text field's text observable, it emits the base value which is an empty string. Because of this, you need to skip the first value emitted by the Observable and replace it with the output from your getUserData() function.
let latestUserData = Observable.combineLatest(
Observable.merge(firstName.rx.text.orEmpty.skip(1), userData.map(\.firstName)),
Observable.merge(lastName.rx.text.orEmpty.skip(1), userData.map(\.lastName)),
Observable.merge(email.rx.text.orEmpty.skip(1), userData.map(\.email))
) { UserData(firstName: $0, lastName: $1, email: $2) }
The above is a lot of code, so let's dwell on it a bit... For each field, I'm grabbing whatever comes out of our getUserData() and merging it with whatever comes out of the appropriate text field. I'm ignoring the first value that comes out of the text field though, because I know the user didn't type that in.
But you only want to call the save function if the user taps the sendButton. So that's your trigger:
sendButton.rx.tap
.withLatestFrom(latestUserData)
.subscribe(onNext: { save($0) })
.disposed(by: disposeBag)
Here's all the code in one place, including an observe(on:) which is necessary if your network getter emits its value on a background thread:
override func viewDidLoad() {
super.viewDidLoad()
let userData = getUserData()
.observe(on: MainScheduler.instance)
.share()
let latestUserData = Observable.combineLatest(
Observable.merge(firstName.rx.text.orEmpty.skip(1), userData.map(\.firstName)),
Observable.merge(lastName.rx.text.orEmpty.skip(1), userData.map(\.lastName)),
Observable.merge(email.rx.text.orEmpty.skip(1), userData.map(\.email))
) { UserData(firstName: $0, lastName: $1, email: $2) }
sendButton.rx.tap
.withLatestFrom(latestUserData)
.subscribe(onNext: { save($0) })
.disposed(by: disposeBag)
disposeBag.insert(
userData.map(\.firstName).bind(to: firstName.rx.text),
userData.map(\.lastName).bind(to: lastName.rx.text),
userData.map(\.email).bind(to: email.rx.text)
)
}
If you want a view model, then here you go:
func saveViewModel(trigger: Observable<Void>, firstName: Observable<String?>, lastName: Observable<String?>, email: Observable<String?>, initial: Observable<UserData>) -> Observable<UserData> {
let latestUserData = Observable.combineLatest(
Observable.merge(firstName.compactMap { $0 }.skip(1), initial.map(\.firstName)),
Observable.merge(lastName.compactMap { $0 }.skip(1), initial.map(\.lastName)),
Observable.merge(email.compactMap { $0 }.skip(1), initial.map(\.email))
) { UserData(firstName: $0, lastName: $1, email: $2) }
return trigger
.withLatestFrom(latestUserData)
}
You can easily test the above with RxTest without bringing in anything from UIKit or your network stack. Bind its output to the save function like this:
saveViewModel(
trigger: sendButton.rx.tap.asObservable(),
firstName: firstName.rx.text.asObservable(),
lastName: lastName.rx.text.asObservable(),
email: email.rx.text.asObservable(),
initial: userData
)
.subscribe(onNext: { save($0) })
.disposed(by: disposeBag)