I need request different types of models from network and then combine them into one model.
How is it possible to chain multiple observables and return another observable?
I have something like:
func fetchDevices() -> Observable<DataResponse<[DeviceModel]>>
func fetchRooms() -> Observable<DataResponse<[RoomModel]>>
func fetchSections() -> Observable<DataResponse<[SectionModel]>>
and I need to do something like:
func fetchAll() -> Observable<(AllModels, Error)> {
fetchSections()
// Then if sections is ok I need to fetch rooms
fetchRooms()
// Then - fetch devices
fetchDevices()
// And if everything is ok create AllModels class and return it
// Or return error if any request fails
return AllModels(sections: sections, rooms: rooms, devices:devices)
}
How to achieve it with RxSwift? I read docs and examples but understand how to chain observables with same 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)
I think the methods that fetching models should reside in ViewModel, and an event should be waiting for start calling them altogether, or they won't start running.
Assume that there's a button calls your three methods, and one more button that will be enabled if the function call is succeeded.
Consider an ViewModel inside your ViewController.
let viewModel = ViewModel()
In ViewModel, declare your abstracted I/O event like this,
struct Input {
buttonTap: Driver<Void>
}
struct Output {
canProcessNext: Driver<Bool>
}
Then you can clearly transform your Input into Output by making function like this in ViewModel.
func transform(input: Input) -> Output {
// TODO: transform your button tap event into fetch result.
}
At viewDidLoad,
let output = viewModel.transform(input: yourButton.rx.tap.asDriver())
output.drive(nextButton.rx.isEnabled).disposed(by: disposeBag)
Now everything's ready but combining your three methods - put them in ViewModel.
func fetchDevices() -> Observable<DataResponse<[DeviceModel]>>
func fetchRooms() -> Observable<DataResponse<[RoomModel]>>
func fetchSections() -> Observable<DataResponse<[SectionModel]>>
Let's finish the 'TODO'
let result = input.buttonTap.withLatestFrom(
Observable.combineLatest(fetchDevices(), fetchRooms(), fetchSections()) { devices, rooms, sections in
// do your job with response data and refine final result to continue
return result
}.asDriver(onErrorJustReturn: true))
return Output(canProcessNext: result)
I'm not only writing about just make it work, but also considering whole design for your application. Putting everything inside ViewController is not a way to go, especially using Rx design. I think it's a good choice to dividing VC & ViewModel login for future maintenance. Take a look for this sample, I think it might help you.
Related
I am using RxSwift and Swinject in my project. The way I bind the inputs / outputs is not exactly the same as in the example given by RxSwift. In RxExample/GitHubSignup, the binding is done in the init(), right? But I found it difficult to implement because I use Swinject+SwinjectStoryboard to do Dependency Injection to the View Controller. Thus, the init() is unavailable because the one instantiating the View Models is the Swinject container. So, is there a way to bind the view controller and view model together besides using init()?
I was thinking that I can maybe use var instead of let for the output observables and make a func bind(observables: [Observable]) or something that will do the bindings and transformations from input to output instead. But because they will be vars and not lets, that means it seems like we are permitted to change the bindings throughout the code. Unlike when we just use lets and bind them in the init(). And also, by using a function instead of the initializer, I have to store the dependencies into a member variable. While if I use the initializer, I can just transform the dependencies inside of a map or flatMap.
And also I have another question. Say, if I have this:
class MyViewController: UIViewController {
#IBOutlet weak var refreshButton: UIButton!
#IBOutlet weak var tableView: UITableView!
var viewModel: MyViewModel!
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
viewModel = MyViewModel(refreshTap: refreshButton.rx.tap, dataProvider: ApiAdapter().getData)
}
private func setupEvents() {
viewModel.tableDTOs.bind(to: tableView.rx.items(
cellIdentifier: reuseId, cellType: TableViewCell.self)) { _, dto, cell in
cell.fill(with: dto)
}.disposed(by: disposeBag)
}
}
final class MyViewModel {
let tableDTOs: Observable<[TableDTO]>
init(refreshTap: Observable<Void>, dataProvider: () -> Observable<[TableDTO]>) {
tableDTOs = Observable.merge(.just(), refreshTap) //Merge with .just to emit at once for initial values
.flatMapLatest { dataProvider().asDriver() }
}
}
So in this case, if the dataProvider returned complete or error, the disposable will be disposed, right? So the scene will be unresponsive because the UI is already unbound. Any idea how to fix that?
Thanks.
So, is there a way to bind the view controller and view model together besides using init()?
Yes there is. Give the view model a function that takes the inputs and returns the outputs.
But because they will be vars and not lets, that means it seems like we are permitted to change the bindings throughout the code.
Don't ever make an Observable (or Subject, or Observer) a var always use let Functional Reactive Programming is a functional paradigm so no vars.
So in this case, if the dataProvider returned complete or error, the disposable will be disposed, right? So the scene will be unresponsive because the UI is already unbound. Any idea how to fix that?
Yes and no. If the dataProvider emits a completed event, that will not dispose because the flatMapLatest only disposes if all of its inputs complete. Since the refreshTap has not yet completed, the flatMapLatest will continue to accept events from it and call it's closure for each one.
If the dataProvider emits an error event, that will dispose because and error event short circuits the chain. However, since you use .asDriver() on your dataProvider, the Driver returned from the closure can't possibly emit an error event. You're safe.
Other ways of stopping the error from breaking the chain are to use .materialize() or any of the .catchError operators. For example:
.flatMapLatest {
dataProvider
.map { Result<[TableDTO], Error>.success($0) }
.catchError { Observable.just(Result<[TableDTO], Error>.failure($0) }
}
I have a view structured like a form that creates a model object. I am trying to bind the form elements (UIControl) to the model properties, so that the views auto-update when their corresponding model property is changed, and the model update when the controls are changed (two way binding). The model can change without the view knowing because multiple views can be linked to one same model property.
Approach 1: Plain Swift
My problem is the following: to observe changes to the model properties, I tried to use KVO in Swift, and specifically the observe(_:changeHandler:) method.
class Binding<View: NSObject, Object: NSObject, ValueType> {
weak var object: Object?
weak var view: View?
var objectToViewObservation: NSKeyValueObservation?
var viewToObjectObservation: NSKeyValueObservation?
private var objectKeyPath: WritableKeyPath<Object, ValueType>
private var viewKeyPath: WritableKeyPath<View, ValueType>
init(betweenObject objectKeyPath: WritableKeyPath<Object, ValueType>,
andView viewKeyPath: WritableKeyPath<View, ValueType>) {
self.objectKeyPath = objectKeyPath
self.viewKeyPath = viewKeyPath
}
override func bind(_ object: Object, with view: View) {
super.bind(object, with: view)
self.object = object
self.view = view
// initial value from object to view
self.view![keyPath: viewKeyPath] = self.object![keyPath: objectKeyPath]
// object --> view
objectToViewObservation = object.observe(objectKeyPath) { _, change in
guard var view = self.view else {
// view doesn't exist anymore
self.objectToViewObservation = nil
return
}
guard let value = change.newValue else { return }
view[keyPath: self.viewKeyPath] = value
}
// view --> object
viewToObjectObservation = view.observe(viewKeyPath) { _, change in
guard var object = self.object else {
// object doesn't exist anymore
self.viewToObjectObservation = nil
return
}
guard let value = change.newValue else { return }
object[keyPath: self.objectKeyPath] = value
}
}
}
However some of the properties of my model have types CustomEnum, CustomClass, Bool?, and ClosedRange<Int>, and to use observe I had to mark them as #objc dynamic, which yielded the error:
Property cannot be marked #objc because its type cannot be represented in Objective-C
Approach 2: Using RxSwift rx.observe
I turned to RxSwift and the rx.observe method thinking I could work around this problem, but the same thing happened (at runtime this time).
// In some generic bridge class between the view and the model
func bind(to object: SomeObjectType) {
object.rx
.observe(SomeType.self, "someProperty")
.flatMap { Observable.from(optional: $0) }
.bind(to: self.controlProperty)
.disposed(by: disposeBag)
}
Approach 3: Using RxSwift BehaviorRelays?
This is my first experience with RxSwift, and I know I should be using BehaviorRelay for my model, however I don't want to change all my model properties as my model object is working with other framework. I could try to implement a bridge then, to transform model properties into BehaviorRelay, but I would come across the same problem: how to listen for model changes.
In this question, there were no answer as to how to listen for property changes without refactoring all model properties to RxSwift's Variable (currently deprecated).
Approach 4: Using didSet Swift property observer?
The didSet and willSet property observers in plain Swift would allow listening for changes, however this would require to mark all the properties in the model with these observers, which I find quite inconvenient, since my model object has a lot of properties. If there is a way to add these observers at runtime, this would solve my problem.
I believe that what I am trying to achieve is quite common, having a set of views that modify a model object, however I can't find a way to properly link the model to the view, so that both auto-update when needed.
Basically, I'm looking for an answer to one of the following questions:
Is there something I overlooked, is there a better way to achieve what I want?
or How to overcome the "Property cannot be marked #objc" problem?
or How to bridge my model object to BehaviorRelay without changing my model?
or How to add didSet observers at runtime?
You said:
I believe that what I am trying to achieve is quite common, having a set of views that modify a model object, however I can't find a way to properly link the model to the view, so that both auto-update when needed.
Actually it's not at all common. One idea you don't mention is to wrap your entire model into a Behavior Relay. Then the set of views can modify your model object.
Each of your views, in turn, can observe the model in the behavior relay and update accordingly. This is the basis of, for example, the Redux pattern.
You could also use your approach #3 and use property wrappers to make the code a bit cleaner:
#propertyWrapper
struct RxPublished<Value> {
private let relay: BehaviorRelay<Value>
public init(wrappedValue: Value) {
self.relay = BehaviorRelay(value: wrappedValue)
}
var wrappedValue: Value {
get { relay.value }
set { relay.accept(newValue) }
}
var projectedValue: Observable<Value> {
relay.asObservable()
}
}
But understand that the whole reason you are having this problem is not due to Rx itself, but rather due to the fact that you are trying to mix paradigms. You are increasing the complexity of your code. Hopefully, it's just a temporary increase during a refactoring.
Old Answer
You said you want to make it "so that the views auto-update when their corresponding model property is changed, and the model update when the controls are changed (two way binding)."
IMO, that way of thinking about the problem is incorrect. Better would be to examine each output independently of all other outputs and deal with it directly. In order to explain what I mean, I will use the example of converting °F to °C and back...
This sounds like a great reason to use 2-way binding but let's see?
// the chain of observables represents a view model
celsiusTextField.rx.text // • this is the input view
.orEmpty // • these next two convert
.compactMap { Double($0) } // the view into an input model
.map { $0 * 9 / 5 + 32 } // • this is the model
.map { "\($0)" } // • this converts the model into a view
.bind(to: fahrenheitTextField) // • this is the output view
.disposed(by: disposeBag)
fahrenheitTextField.rx.text
.orEmpty
.compactMap { Double($0) }
.map { ($0 - 32) * 5 / 9 }
.map { "\($0)" }
.bind(to: celsiusTextField.rx.text)
.disposed(by: disposeBag)
The above code handles the two-way communication between the text fields without two-way binding. It does this by using two separate view models (The view model is the code between the text Observable and the text Observer as described in the comments.)
We can see a lot of duplication. We can DRY it up a bit:
extension ControlProperty where PropertyType == String? {
func viewModel(model: #escaping (Double) -> Double) -> Observable<String> {
orEmpty
.compactMap { Double($0) }
.map(model)
.map { "\($0)" }
}
}
You may prefer a different error handling strategy than what I used above. I was striving for simplicity since this is an example.
The key though is that each observable chain should be centered on a particular effect. It should combine all the causes that contribute to that effect, implement some sort of logic on the inputs, and then emit the needed output for that effect. If you do this to each output individually you will find that you don't need two-way binding at all.
I'm working on an iOS application adopting the MVVM pattern, using SwiftUI for designing the Views and Swift Combine in order to glue together my Views with their respective ViewModels.
In one of my ViewModels I've created a Publisher (type Void) for a button press and another one for the content of a TextField (type String).
I want to be able to combine both Publishers within my ViewModel in a way that the combined Publisher only emits events when the button Publisher emits an event while taking the latest event from the String publisher, so I can do some kind of evaluation on the TextField data, every time the user pressed the button. So my VM looks like this:
import Combine
import Foundation
public class MyViewModel: ObservableObject {
#Published var textFieldContent: String? = nil
#Published var buttonPressed: ()
init() {
// Combine `$textFieldContent` and `$buttonPressed` for evaulation of textFieldContent upon every button press...
}
}
Both publishers are being pupulated with data by SwiftUI, so i will omit that part and let's just assume both publishers receive some data over time.
Coming from the RxSwift Framework, my goto solution would have been the withLatestFrom operator to combine both observables.
Diving into the Apple Documentation of Publisher in the section "Combining Elements from Multiple Publishers" however, I cannot find something similar, so I expect this kind of operator to be missing currently.
So my question: Is it possible to use the existing operator-API of the Combine Framework to get the same behavior in the end like withLatestFrom?
It sounds great to have a built-in operator for this, but you can construct the same behavior out of the operators you've got, and if this is something you do often, it's easy to make a custom operator out of existing operators.
The idea in this situation would be to use combineLatest along with an operator such as removeDuplicates that prevents a value from passing down the pipeline unless the button has emitted a new value. For example (this is just a test in the playground):
var storage = Set<AnyCancellable>()
var button = PassthroughSubject<Void, Never>()
func pressTheButton() { button.send() }
var text = PassthroughSubject<String, Never>()
var textValue = ""
let letters = (97...122).map({String(UnicodeScalar($0))})
func typeSomeText() { textValue += letters.randomElement()!; text.send(textValue)}
button.map {_ in Date()}.combineLatest(text)
.removeDuplicates {
$0.0 == $1.0
}
.map {$0.1}
.sink { print($0)}.store(in:&storage)
typeSomeText()
typeSomeText()
typeSomeText()
pressTheButton()
typeSomeText()
typeSomeText()
pressTheButton()
The output is two random strings such as "zed" and "zedaf". The point is that text is being sent down the pipeline every time we call typeSomeText, but we don't receive the text at the end of the pipeline unless we call pressTheButton.
That seems to be the sort of thing you're after.
You'll notice that I'm completely ignoring what the value sent by the button is. (In my example it's just a void anyway.) If that value is important, then change the initial map to include that value as part of a tuple, and strip out the Date part of the tuple afterward:
button.map {value in (value:value, date:Date())}.combineLatest(text)
.removeDuplicates {
$0.0.date == $1.0.date
}
.map {($0.value, $1)}
.map {$0.1}
.sink { print($0)}.store(in:&storage)
The point here is that what arrives after the line .map {($0.value, $1)} is exactly like what withLatestFrom would produce: a tuple of both publishers' most recent values.
As improvement of #matt answer this is more convenient withLatestFrom, that fires on same event in original stream
Updated: Fix issue with combineLatest in iOS versions prior to 14.5
extension Publisher {
func withLatestFrom<P>(
_ other: P
) -> AnyPublisher<(Self.Output, P.Output), Failure> where P: Publisher, Self.Failure == P.Failure {
let other = other
// Note: Do not use `.map(Optional.some)` and `.prepend(nil)`.
// There is a bug in iOS versions prior 14.5 in `.combineLatest`. If P.Output itself is Optional.
// In this case prepended `Optional.some(nil)` will become just `nil` after `combineLatest`.
.map { (value: $0, ()) }
.prepend((value: nil, ()))
return map { (value: $0, token: UUID()) }
.combineLatest(other)
.removeDuplicates(by: { (old, new) in
let lhs = old.0, rhs = new.0
return lhs.token == rhs.token
})
.map { ($0.value, $1.value) }
.compactMap { (left, right) in
right.map { (left, $0) }
}
.eraseToAnyPublisher()
}
}
Kind-of a non-answer, but you could do this instead:
buttonTapped.sink { [unowned self] in
print(textFieldContent)
}
This code is fairly obvious, no need to know what withLatestFrom means, albeit has the problem of having to capture self.
I wonder if this is the reason Apple engineers didn't add withLatestFrom to the core Combine framework.
I know state is the enemy of Reactive programming but I'm dealing with it in my process of learning RxSwift.
My app is very simple, the first screen is a list and a search of books and the second a detail of the book in which you can add/remove a book to your shelf and mark it as read/unread.
To show the detail of the book I create a BookViewModel passing a BooksService to perform network operations and the current Book to show.
The problem is that I have to keep track of the changes in the book in order to change the UI: for example, after removing the book the button that previously says "Remove" now it has to say "Add".
I achieve this behavior using a Variable<Book> exposed to the observers as a Driver<Book>, but I'm messing a lot with it when the network operation returns and I have to update the value of the Variable<Book> in order to trigger the update of the UI.
This is the initializer of the view model:
init(book: Book, booksService: BooksService) {
self._book = Variable(book)
self.booksService = booksService
}
This is the observable I expose
var book: Driver<Book> {
return _book.asDriver()
}
And here it is my function to add/remove the book:
func set(toggleInShelfTrigger: Observable<Void>) {
toggleInShelfTrigger // An observable from a UIBarButtonItem tap
.map({ self._book.value }) // I have to map the variable's value to the actual book
.flatMap({ [booksService] book -> Observable<Book> in
return (book.isInShelf ?
booksService.delete(book: book) :
booksService.add(book: book))
}) // Here I have to know if the books is in the shelf or not in order to perform one operation or another.
.subscribe(onNext: { self._book.value = $0 }) // I have to update the variable's value in order to trigger the UI update
.disposed(by: disposeBag)
}
I am very unhappy with this code and the whole view model. It works but it is clunky, and essentially wrong because if the network operation fails the subscription will be disposed and my button will became unresponsive.
If I get rid of the Variable<Book> and return a Driver<Book> from the method set(toggleInShelfTrigger: Observable<Void>) I won't have this mess but I will not be able to know if I have to add or to remove the book.
So, what is the real world way to keep track of the state of an object in this kind of app? How can I achieve this by only using Rx operators?
EDIT
I've managed to clean that crappy code but I'm still trying to achieve state without Variable and using scan operator.
This is my new BookViewModel initializer:
init(book: Book, booksService: BooksService) {
self.bookVariable = Variable(book)
let addResult = addBook
.mapBookFrom(bookVariable)
.flatMapLatest({ booksService.add(book: $0) })
.updateBookVariable(bookVariable)
let removeResult = ... // Similar to addResult, changing service call
let markReadResult = ... // Similar to addResult, changing service call
let markUnreadResult = ... // Similar to addResult, changing service call
self.book = Observable.of(addResult, removeResult, markReadResult, markUnreadResult).merge()
.startWith(.success(book))
}
I made a couple of custom operators to help me manage the Variable<Book>, one to get the real Book:
private extension ObservableType where E == Void {
func mapBookFrom(_ variable: Variable<Book>) -> Observable<Book> {
return map({ _ in return variable.value })
}
}
And another to update the Variable after the service returns:
private extension ObservableType where E == BookResult<Book> {
func updateBookVariable(_ variable: Variable<Book>) -> Observable<BookResult<Book>> {
return self.do(onNext: { result in
if case let .success(book) = result {
variable.value = book
}
})
}
}
Now I have a very clean view model, but not "perfect".
I would place the responsibility of the observing changes to the model object (Book) with the View.
Also, Variable is deprecated, best to use PublishRelay instead.
Of course, it depends how far you want to engineer this architecture, but something not too far from your example would be:
class BookDetailViewController: UIViewController {
let viewModel = BookViewModel(book: Book, booksService: BooksService)
func loadView() {
view = BookDetailView(viewModel: viewModel)
}
// ...
}
class BookDetailViewModel {
let book: PublishRelay<Book>
func addBook() {
book
.flatMap(booksService.add)
.bind(to: book)
.subscribe()
}
// ...
}
class BookDetailView: UIView {
let button: UIButton
init(viewModel: BookDetailViewModel) {
viewModel.book
.asObservable()
.subscribe(onNext: { book [button] in
button.setText(book.isSaved ? "Remove" : "Add")
})
button.rx.tap
.map { _ in viewModel.book.isSaved }
.subscribe(onNext: {
$0 ? viewModel.removeBook() : viewModel.addBook()
})
}
}
You could also implement a func toggle() in the view model instead, and just forward the button tap to call that method. It might be more accurate, semantically, depending on your interpretation of business logic and the extent to which you want to gather all of it in the view model.
Also note the example code is missing dispose bags, but that's another topic.
My question is very similar to several others here but I just can't get it to work. I'm making an API call via a helper class that I wrote.
First I tried a standard function with a return value and the result was as expected. The background task completed after I tired to assign the result.
Now I'm using a closure and I can get the value back into my view controller but its still stuck in the closure, I have the same problem. I know I need to use GCD to get the assignment to happen in the main queue.
this is what I have in my view controller
var artists = [String]()
let api = APIController()
api.getArtistList("foo fighters") { (thelist) -> Void in
if let names = thelist {
dispatch_async(dispatch_get_main_queue()) {
artists = names
print("in the closure: \(artists)")
}
}
}
print ("method 1 results: \(artists)")
as the results are:
method 1 results: []
in the closure: [Foo Fighters & Brian May, UK Foo Fighters, John Fogerty with Foo Fighters, Foo Fighters, Foo Fighters feat. Norah Jones, Foo Fighters feat. Brian May, Foo Fighters vs. Beastie Boys]
I know why this is happening, I just don't know how to fix it :( The API calls need to be async, so what is the best practice for capturing these results? Based on what the user selects in the table view I'll be making subsequent api calls so its not like I can handle everything inside the closure
I completely agree with the #Craig proposal of the use of the GCD, but as your question involves the request of the API call every time you select a row, you can do the following:
Let's suppose you use the tableView:didSelectRowAtIndexPath: method to handle the selection, then you can do the following inside it:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// it is just a form to get the item
let selectedItem = items.objectAtIndex(indexPath.row) as String
api.getArtistList(selectedItem) { (thelist) -> Void in
if let names = thelist {
dispatch_async(dispatch_get_main_queue()) {
artists = names
}
}
}
}
And then you can observe the property and handle do you want inside it :
var artists: [String] = [] {
didSet {
self.tableView.reloadData() // or anything you need to handle.
}
}
It just another way to see it. I hope this help you.
The easy solution is to do whatever you're doing at your print(), inside the closure.
Since you're already dispatch_asyncing to the main queue (the main/GUI thread), you can complete any processing there. Push a new view controller, present some modal data, update your current view controller, etc.
Just make sure that you don't have multiple threads modifying/accessing your local/cached data that is being displayed. Especially if it's being used by UITableViewDelegate / UITableViewDataSource implementations, which will throw fits if you start getting wishy-washy or inconsistent with your return values.
As long as you can retrieve the data in the background, and the only processing that needs to occur on the main thread is an instance variable reassignment, or some kind of array appending, just do that on the main thread, using the data you retrieved on the back end. It's not heavy. If it is heavy, then you're going to need more sophisticated synchronization methods to protect your data.
Normally the pattern looks like:
dispatch_async(getBackgroundQueue(), {
var theData = getTheDataFromNetwork();
dispatch_async(dispatch_get_main_queue() {
self.data = theData // Update the instance variable of your ViewController
self.tableView.reloadData() // Or some other 'reload' method
});
})
So where you'd normally refresh a table view or notify your ViewController that the operation has completed (or that local data has been updated), you should continue your main-thread processing.