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 Driver
s)
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
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.
- 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... Theshare()
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)
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
Make Touch-Area for Sklabelnode Bigger for Small Characters
Guard Let Error: Initializer for Conditional Binding Must Have Optional Type Not 'String'
How to Place Uisearchcontroller to the Navigationtitle and How to Enable and Disable It by Button
Loading Views into Nscontainerview with Swift
Integrating Congnito User Pools with Amazon Cognito Identity
Coerced to Any' But Property Is of Type Uicolor
How to Re-Order the Realm Table Using Tableview in Swift
Firestore Query Across Multiple Fields for Same Value
Cannot Convert Double to String
Swift, for Some Uiviews to Their Overall Controller When Clicked
Getting Twitter Profile Image with Parse in Swift
Extending a Protocol Where Self: Generic Type in Swift (Requires Arguments in <...>)
Swift How to Convert Parse Createdat Time to Time Ago