Simple Observable Struct with Rxswift

Simple observable struct with RxSwift?

Easiest way to quickly make this observable with RxSwift would probably be to use the RxSwift class Variable (all code here is untested off the top of my head):

import RxSwift

class Car {

var miles = Variable<Int>(0)

var name = Variable<String>("Turbo")

}

This enables you to observe the values by subscribing to them:

let disposeBag = DisposeBag()
let car = Car
car.name.asObservable()
.subscribeNext { name in print("Car name changed to \(name)") }
.addToDisposeBag(disposeBag) // Make sure the subscription disappears at some point.

Now you've lost the old value in each event. There are of course numerous ways to solve this, the RxSwifty way would probably be to add a scan operation to your element sequence, which works a lot like reduce does on a normal Array:

car.name.asObservable()
.scan(seed: ("", car.name.value)) { (lastEvent, newElement) in
let (_, oldElement) = lastEvent
return (oldElement, newElement)
}
.subscribeNext { (old, new) in print("Car name changed from \(old) to \(new)") }
.addToDisposeBag(disposeBag)

How to turn a standard model class/struct into observable properties?

You don't need to create another object to make model properties observable. But there is definitely an overhead of implementing the decoder & encoder methods and coding keys enum as below which is also a standard practice in many cases,

class User: Codable {
var id: String = ""
var firstName = Observable<String?>(nil)
var avatar = Observable<String?>(nil)

private enum CodingKeys: String, CodingKey {
case id, firstName, avatar
}

required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.firstName.value = try container.decode(String.self, forKey: .firstName)
self.avatar.value = try container.decode(String.self, forKey: .avatar)
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.firstName.value, forKey: .firstName)
try container.encode(self.avatar.value, forKey: .avatar)
}
}

Now you are able to bind any UIView element.


Normally, with reactive approach, ViewModel is where you create bindable properties that provide value from your model to view and update model property by keeping the model invisible by the view.

RxSwift Observe variable fields of struct type in array

First of all, you should not be using Variable anymore, as it's deprecated. You should use BehaviorRelay instead.

I'm not sure I understand your question clearly. But you probably need something like this:

// namesNotEmpty will be true if all elements in array have name.count > 0
let namesNotEmpty: Observable<Bool> = array.asObservable()
.flatMap { array -> Observable<[String]> in
if array.isEmpty {
// so that some event is emmited when array is empty
return Observable.just([""])
}

return Observable.combineLatest(array.map { $0.name.asObservable() })
}
.map { array in
array.filter { $0.isEmpty }.isEmpty
}

In RxSwift how can I set up a Subject to observe another Observable?

The answer is

theObservable
.subscribe(onNext: { theSubject.onNext($0) })
.disposed(by: disposeBag)

This will make sure that every time that the theObservable emits, the value will be passed to theSubject too.

Note
This only passes the value onNext, if you want to handle all the cases, then use bind(to:) as the answer by Daniel T. (or drive for Drivers)

Example with more Observables

In the following example values from different Observables will be passed to theSubject

let theSubject = PublishSubject<String>()
let theObservable = Observable.just("Hello?")
let anotherObservable = Observable.just("Hey there")

theSubject.asObservable()
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

theObservable
.subscribe(onNext: { theSubject.onNext($0) })
.disposed(by: disposeBag)

anotherObservable
.subscribe(onNext: { theSubject.onNext($0) })
.disposed(by: disposeBag)

Output

The subject

How to implement simple form with UITextFields and RxSwift?

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.

  1. Filling out the initial value for firstName:
getUserData()
.map(\.firstName)
.bind(to: firstName.rx.text)
.disposed(by: disposeBag)

Well that was simple.


  1. 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)
)

  1. 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)
)

  1. 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)

Swift 5: Return a tuple in observable with RxSwift

Just swap the subscribe out for a map. Something like the below:

func getStatus() -> Observable<(MessageStatus, Bool)> {
let query = Bundle.main.query ?? ""
let body = ["query": "\(query)"]

return response(body: body)
.map { try JSONDecoder().decode(Data.self, from: $0) }
.map { response in
guard response.data != nil else {
let error = response.errors?.data?.first
let failure = MessageStatus(message: error.message , code: error.code)
return (failure, false)
}
let success = MessageStatus(message: "Success", code: 200)
return (success, true)
}
}

Chaining RxSwift observable with different type

Try combineLatest operator. You can combine multiple observables:

let data = Observable.combineLatest(fetchDevices, fetchRooms, fetchSections) 
{ devices, rooms, sections in
return AllModels(sections: sections, rooms: rooms, devices:devices)
}
.distinctUntilChanged()
.shareReplay(1)

And then, you subscribe to it:

data.subscribe(onNext: {models in 
// do something with your AllModels object
})
.disposed(by: bag)


Related Topics



Leave a reply



Submit