I have an object created from flatMaping an observable.
private lazy var childObj: chilView? = {
let keychainStore = Realm().getStore()
let selectedElementID = keychainStore.elementID
.asObservable()
.distinctUntilChanged {$0 == $1}
.flatMap({ (elementID) -> Observable<Element?> in
guard let elementID = elementID else {
return Observable.error(Errors.InvalidElementID)
}
return Observable.create { observer in
let elementStream: Observable<Result<Element>> = keychainStore.getObservable(id: elementID)
elementStream.subscribe(onNext: { (result) in
switch result {
case .success(let element):
observer.onNext(element)
default: break
}
})
.disposed(by: self.disposeBag)
return Disposables.create()
}
})
return self.createChildObject(with: selectedElementID)
}()
The selectedElement is of type flatMap observable. createChildObject(with:) is called even before observer.onNext(element) is executed. How do I fix this?
I am not sure I completely understand your problem from the description but here are some thoughts:
It's not a race condition. Your selectedElementID chain and the flatMap code within it will not be executed before you subscribe to the selectedElementID. So I guess you subscribe to the selectedElementID somewhere within createChildObject method and obviously get the createChildObject code executed before the selectedElementID chain.
Depending on how keychainStore rx calls are designed and how you subscribe to selectedElementID observable you might get a race condition but I am not sure that the using RxSwift is a good decision in this case. Try making your chains more atomic.
Related
Hi So i am trying to wrap an async network request with Combine's Future/Promise.
my code goes something like that:
enum State {
case initial
case loading
case success(movies: [Movie])
case error(message: String)
}
protocol ViewModelProtocol {
var statePublisher: AnyPublisher<State, Never> { get }
func load(genreId: String)
}
class ViewModel: ViewModelProtocol {
var remoteDataSource = RemoteDataSource()
#Published state: State = .initial
var statePublisher: AnyPublisher<State, Never> { $state.eraseToAnyPubliher() }
public func load(genreId: String) {
self.state = .loading
self.getMovies(for: genreId)
.sink { [weak self] (moveis) in
guard let self = self else { return }
if let movies = movies {
self.state = .success(movies: movies)
} else {
self.state = .error(message: "failed to load movies")
}
}
}
func getMovies(for genreId: String) -> AnyPublisher<[Movie]?, Never> {
Future { promise in
self.remoteDataSource.getMovies(for: genreId) { (result) in
switch result {
case .success(let movies): promise(.success(movies))
case .failure: promise(.success(nil))
}
}
}.eraseToAnyPublisher()
}
}
I was trying to see if there are any memory leaks and found that there is a reference to the Future that is not being deallocated
same as here: Combine Future Publisher is not getting deallocated
You are strongly capturing self inside of your escaping Future init (just capture remoteDataSource). It doesn't seem to me like this should cause a memory leak. As the link you put in the question suggests Future does not behave like most other publishers; it does work as soon as you create it and not when you subscribe. It also memoizes and shares its results. I strongly suggest that you do no use this behind an AnyPublisher because its very non obvious to the caller that this thing is backed by a Future and that it will start work immediately. I would use URLSession.shared.dataTaskPublisher instead and then you get a regular publisher and you don't need the completion handler or the Future. Otherwise wrap the Future in a Deferred so you don't get the eager evaluation.
I have a BehaviorSubject where my tableview is bound to through RxDataSources.
Besides that, I have a pull to refresh which creates an observable that updates the data and updates the data in the BehaviorSubject so that my UITableView updates correctly.
Now the question is, how do I handle the error handling for whenever my API call fails?
Few options that I have thought of was:
Subscribe to the observer's onError and call the onError of my BehaviorSubject\
Somehow try to concat? or bind(to: ..)
Let another subscriber in my ViewController subscribe besides that my tableview subscribes to the BehaviorSubject.
Any suggestions?
Ideally, you wouldn't use the BehaviorSubject at all. From the Intro to Rx book:
The usage of subjects should largely remain in the realms of samples and testing. Subjects are a great way to get started with Rx. They reduce the learning curve for new developers, however they pose several concerns...
Better would be to do something like this in your viewDidLoad (or a function that is called from your viewDidLoad):
let earthquakeData = Observable.merge(
tableView.refreshControl!.rx.controlEvent(.valueChanged).asObservable(),
rx.methodInvoked(#selector(UIViewController.viewDidAppear(_:))).map { _ in }
)
.map { earthquakeSummary /* generate URLRequest */ }
.flatMapLatest { request in
URLSession.shared.rx.data(request: request)
.materialize()
}
.share(replay: 1)
earthquakeData
.compactMap { $0.element }
.map { Earthquake.earthquakes(from: $0) }
.map { $0.map { EarthquakeCellDisplay(earthquake: $0) } }
.bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: EarthquakeTableViewCell.self)) { _, element, cell in
cell.placeLabel.text = element.place
cell.dateLabel.text = element.date
cell.magnitudeLabel.text = element.magnitude
cell.magnitudeImageView.image = element.imageName.isEmpty ? UIImage() : UIImage(named: element.imageName)
}
.disposed(by: disposeBag)
earthquakeData
.compactMap { $0.error }
.map { (title: "Error", message: $0.localizedDescription) }
.bind { [weak self] title, message in
self?.presentAlert(title: title, message: message, animated: true)
}
.disposed(by: disposeBag)
The materialize() operator turns a Event.error(Error) result into an Event.next(.error(Error)) so that the chain won't be broken down. The .compactMap { $0.element } emits only the successful results while the .compactMap { $0.error } emits only the errors.
The above code is adapted from my RxEarthquake sample.
I'm new to RXSwift and I try to use combineLatest to combine the latest results from two public subjects
What I tried to do:
let sub1 = PublicSubject<Type1>()
let sub2 = PublicSubject<Type2>()
NetworkService1.fetch { sub1Value in
sub1.onNext(sub1Value)
}
NetworkService2.fetch { sub21Value in
sub2.onNext(sub2Value)
}
Observable.combineLatest(sub1.asObservable(), sub2.asObservable()) {
val1, val2 in
// do something with val1 and val2
// It seems it never hits this block
}
Not sure I'm doing the right thing.
It's better, if your NetworkService returns Observables. Then you don't have to create PublicSubjects and it's more beautiful.
I would do it like this:
let result1 = NetworkService1.shared.fetch()
let result2 = NetworkService2.shared.fetch()
Observable.combineLatest(result1, result2) { r1, r2 in
// Do stuff with r1, r2 with are APIResult<[YourModel]> and return result
}.subscribe(onNext: { result in
// You need to subscribe to run fetch methods
// Do stuff with result of combine latest
}).disposed(by: disposeBag)
This is example of a fetch method using Alamofire that returns Observable:
func fetch() -> Observable<APIResult<[YourModel]>> {
return Observable<APIResult<[YourModel]>>.create { (observer) -> Disposable in
Alamofire.request(yourURLString,
method: .post,
parameters: nil,
headers: APIManager.headers())
.responseJSON(completionHandler: { dataResponse in
switch dataResponse.result {
case .success(let value):
// parse value to someArray here
observer.onNext(APIResult.success(someArray))
case .failure(_):
guard let code = dataResponse.response?.statusCode else {
observer.onNext(APIResult.failure(APIError.unknownError))
break
}
observer.onNext(APIResult.failure(APIError.networkError(code:code)))
}
observer.onCompleted()
})
return Disposables.create()
}
}
APIResult allows you to pass errors too:
enum APIResult<Value> {
case success(Value)
case failure(APIError)
}
Just a couple of observations first:
There is no PublicSubject, it's PublishSubject (probably just a typo ;)
You don't need to call asObservable() in order to use PublishSubjects as arguments for combineLatest
You have to subscribe to your Observable (in this case Observable.combineLatest), otherwise nothing will happen.
Even if you subscribe correctly to Observable.combineLatest, you won't get the values, that have been emitted before the subscription, so those fetch calls have to be triggered after the subscription.
Since a piece of code is worth a thousand words:
let disposeBag = DisposeBag()
let sub1 = PublishSubject<String>()
let sub2 = PublishSubject<String>()
Observable.combineLatest(sub1, sub2) {
val1, val2 in
// do something with val1 and val2
// IT SHOULD WORK NOW
}.subscribe().disposed(by: disposeBag)
NetworkService1.fetch { sub1Value in
sub1.onNext(sub1Value)
}
NetworkService2.fetch { sub2Value in
sub2.onNext(sub2Value)
}
I am new to RxSwift and MVVM.
my viewModel has a method named rx_fetchItems(for:) that does the heavy lifting of fetching relevant content from backend, and returns Observable<[Item]>.
My goal is to supply an observable property of the viewModel named collectionItems, with the last emitted element returned from rx_fetchItems(for:), to supply my collectionView with data.
Daniel T has provided this solution that I could potentially use:
protocol ServerAPI {
func rx_fetchItems(for category: ItemCategory) -> Observable<[Item]>
}
struct ViewModel {
let collectionItems: Observable<[Item]>
let error: Observable<Error>
init(controlValue: Observable<Int>, api: ServerAPI) {
let serverItems = controlValue
.map { ItemCategory(rawValue: $0) }
.filter { $0 != nil }.map { $0! } // or use a `filterNil` operator if you already have one implemented.
.flatMap { api.rx_fetchItems(for: $0)
.materialize()
}
.filter { $0.isCompleted == false }
.shareReplayLatestWhileConnected()
collectionItems = serverItems.filter { $0.element != nil }.dematerialize()
error = serverItems.filter { $0.error != nil }.map { $0.error! }
}
}
The only problem here is that my current ServerAPI aka FirebaseAPI, has no such protocol method, because it is designed with a single method that fires all requests like this:
class FirebaseAPI {
private let session: URLSession
init() {
self.session = URLSession.shared
}
/// Responsible for Making actual API requests & Handling response
/// Returns an observable object that conforms to JSONable protocol.
/// Entities that confrom to JSONable just means they can be initialized with json.
func rx_fireRequest<Entity: JSONable>(_ endpoint: FirebaseEndpoint, ofType _: Entity.Type ) -> Observable<[Entity]> {
return Observable.create { [weak self] observer in
self?.session.dataTask(with: endpoint.request, completionHandler: { (data, response, error) in
/// Parse response from request.
let parsedResponse = Parser(data: data, response: response, error: error)
.parse()
switch parsedResponse {
case .error(let error):
observer.onError(error)
return
case .success(let data):
var entities = [Entity]()
switch endpoint.method {
/// Flatten JSON strucuture to retrieve a list of entities.
/// Denoted by 'GETALL' method.
case .GETALL:
/// Key (underscored) is unique identifier for each entity, which is not needed here.
/// value is k/v pairs of entity attributes.
for (_, value) in data {
if let value = value as? [String: AnyObject], let entity = Entity(json: value) {
entities.append(entity)
}
}
// Need to force downcast for generic type inference.
observer.onNext(entities as! [Entity])
observer.onCompleted()
/// All other methods return JSON that can be used to initialize JSONable entities
default:
if let entity = Entity(json: data) {
observer.onNext([entity] as! [Entity])
observer.onCompleted()
} else {
observer.onError(NetworkError.initializationFailure)
}
}
}
}).resume()
return Disposables.create()
}
}
}
The most important thing about the rx_fireRequest method is that it takes in a FirebaseEndpoint.
/// Conforms to Endpoint protocol in extension, so one of these enum members will be the input for FirebaseAPI's `fireRequest` method.
enum FirebaseEndpoint {
case saveUser(data: [String: AnyObject])
case fetchUser(id: String)
case removeUser(id: String)
case saveItem(data: [String: AnyObject])
case fetchItem(id: String)
case fetchItems
case removeItem(id: String)
case saveMessage(data: [String: AnyObject])
case fetchMessages(chatroomId: String)
case removeMessage(id: String)
}
In order to use Daniel T's solution, Id have to convert each enum case from FirebaseEndpoint into methods inside FirebaseAPI. And within each method, call rx_fireRequest... If I'm correct.
Id be eager to make this change if it makes for a better Server API design. So the simple question is, Will this refactor improve my overall API design and how it interacts with ViewModels. And I realize this is now evolving into a code review.
ALSO... Here is implementation of that protocol method, and its helper:
func rx_fetchItems(for category: ItemCategory) -> Observable<[Item]> {
// fetched items returns all items in database as Observable<[Item]>
let fetchedItems = client.rx_fireRequest(.fetchItems, ofType: Item.self)
switch category {
case .Local:
let localItems = fetchedItems
.flatMapLatest { [weak self] (itemList) -> Observable<[Item]> in
return self!.rx_localItems(items: itemList)
}
return localItems
// TODO: Handle other cases like RecentlyAdded, Trending, etc..
}
}
// Helper method to filter items for only local items nearby user.
private func rx_localItems(items: [Item]) -> Observable<[Item]> {
return Observable.create { observable in
observable.onNext(items.filter { $0.location == "LA" })
observable.onCompleted()
return Disposables.create()
}
}
If my approach to MVVM or RxSwift or API design is wrong PLEASE do critique.
I know it is tough to start understanding RxSwift
I like to use Subjects or Variables as inputs for the ViewModel and Observables or Drivers as outputs for the ViewModel
This way you can bind the actions that happen on the ViewController to the ViewModel, handle the logic there, and update the outputs
Here is an example by refactoring your code
View Model
// Inputs
let didSelectItemCategory: PublishSubject<ItemCategory> = .init()
// Outputs
let items: Observable<[Item]>
init() {
let client = FirebaseAPI()
let fetchedItems = client.rx_fireRequest(.fetchItems, ofType: Item.self)
self.items = didSelectItemCategory
.withLatestFrom(fetchedItems, resultSelector: { itemCategory, fetchedItems in
switch itemCategory {
case .Local:
return fetchedItems.filter { $0.location == "Los Angeles" }
default: return []
}
})
}
ViewController
segmentedControl.rx.value
.map(ItemCategory.init(rawValue:))
.startWith(.Local)
.bind(to: viewModel.didSelectItemCategory)
.disposed(by: disposeBag)
viewModel.items
.subscribe(onNext: { items in
// Do something
})
.disposed(by: disposeBag)
I think the problem you are having is that you are only going half-way with the observable paradigm and that's throwing you off. Try taking it all the way and see if that helps. For example:
protocol ServerAPI {
func rx_fetchItems(for category: ItemCategory) -> Observable<[Item]>
}
struct ViewModel {
let collectionItems: Observable<[Item]>
let error: Observable<Error>
init(controlValue: Observable<Int>, api: ServerAPI) {
let serverItems = controlValue
.map { ItemCategory(rawValue: $0) }
.filter { $0 != nil }.map { $0! } // or use a `filterNil` operator if you already have one implemented.
.flatMap { api.rx_fetchItems(for: $0)
.materialize()
}
.filter { $0.isCompleted == false }
.shareReplayLatestWhileConnected()
collectionItems = serverItems.filter { $0.element != nil }.dematerialize()
error = serverItems.filter { $0.error != nil }.map { $0.error! }
}
}
EDIT to handle problem mentioned in comment. You now need to pass in the object that has the rx_fetchItems(for:) method. You should have more than one such object: one that points to the server and one that doesn't point to any server, but instead returns canned data so you can test for any possible response, including errors. (The view model should not talk to the server directly, but should do so through an intermediary...
The secret sauce in the above is the materialize operator that wraps error events into a normal event that contains an error object. That way you stop a network error from shutting down the whole system.
In response to the changes in your question... You can simply make the FirebaseAPI conform to ServerAPI:
extension FirebaseAPI: ServerAPI {
func rx_fetchItems(for category: ItemCategory) -> Observable<[Item]> {
// fetched items returns all items in database as Observable<[Item]>
let fetchedItems = self.rx_fireRequest(.fetchItems, ofType: Item.self)
switch category {
case .Local:
let localItems = fetchedItems
.flatMapLatest { [weak self] (itemList) -> Observable<[Item]> in
return self!.rx_localItems(items: itemList)
}
return localItems
// TODO: Handle other cases like RecentlyAdded, Trending, etc..
}
}
// Helper method to filter items for only local items nearby user.
private func rx_localItems(items: [Item]) -> Observable<[Item]> {
return Observable.create { observable in
observable.onNext(items.filter { $0.location == "LA" })
observable.onCompleted()
return Disposables.create()
}
}
}
You should probably change the name of ServerAPI at this point to something like FetchItemsAPI.
You run into a tricky situation here because your observable can throw an error and once it does throw an error the observable sequence errors out and no more events can be emitted. So to handle subsequent network requests, you must reassign taking the approach you're currently taking. However, this is generally not good for driving UI elements such as a collection view because you would have to bind to the reassigned observable every time. When driving UI elements, you should lean towards types that are guaranteed to not error out (i.e. Variable and Driver). You could make your Observable<[Item]> to be let items = Variable<[Item]>([]) and then you could just set the value on that variable to be the array of items that came in from the new network request. You can safely bind this variable to your collection view using RxDataSources or something like that. Then you could make a separate variable for the error message, let's say let errorMessage = Variable<String?>(nil), for the error message that comes from the network request and then you could bind the errorMessage string to a label or something like that to display your error message.
I'm new in RxSwift. Some strange thing happens in my code.
I have a collection view and
Driver["String"]
Data for binding.
var items = fetchImages("flower")
items.asObservable().bindTo(self.collView.rx_itemsWithCellIdentifier("cell", cellType: ImageViewCell.self)) { (row, element, cell) in
cell.imageView.setURL(NSURL(string: element), placeholderImage: UIImage(named: ""))
}.addDisposableTo(self.disposeBag)
fetchImages
Function returns data
private func fetchImages(string:String) -> Driver<[String]> {
let searchData = Observable.just(string)
return searchData.observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
.flatMap
{ text in // .Background thread, network request
return RxAlamofire
.requestJSON(.GET, "https://pixabay.com/api/?key=2557096-723b632d4f027a1a50018f846&q=\(text)&image_type=photo")
.debug()
.catchError { error in
print("aaaa")
return Observable.never()
}
}
.map { (response, json) -> [String] in // again back to .Background, map objects
var arr = [String]()
for i in 0 ..< json["hits"]!!.count {
arr.append(json["hits"]!![i]["previewURL"]!! as! String)
}
return arr
}
.observeOn(MainScheduler.instance) // switch to MainScheduler, UI updates
.doOnError({ (type) in
print(type)
})
.asDriver(onErrorJustReturn: []) // This also makes sure that we are on MainScheduler
}
Strange thing is this. First time when I fetch with "flower" it works and return data, but when I add this code
self.searchBar.rx_text.subscribeNext { text in
items = self.fetchImages(text)
}.addDisposableTo(self.disposeBag)
It doesn't work. It doesn't steps in flatmap callback, and because of this, doesn't return anything.
It works in your first use case, because you're actually using the returned Driver<[String]> via a bindTo():
var items = fetchImages("flower")
items.asObservable().bindTo(...
However, in your second use case, you aren't doing anything with the returned Driver<[String]> other than saving it to a variable, which you do nothing with.
items = self.fetchImages(text)
A Driver does nothing until you subscribe to it (or in your case bindTo).
EDIT: To make this clearer, here's how you could get your second use case to work (I've avoided cleaning up the implementation to keep it simple):
self.searchBar.rx_text
.flatMap { searchText in
return self.fetchImages(searchText)
}
.bindTo(self.collView.rx_itemsWithCellIdentifier("cell", cellType: ImageViewCell.self)) { (row, element, cell) in
cell.imageView.setURL(NSURL(string: element), placeholderImage: UIImage(named: ""))
}.addDisposableTo(self.disposeBag)