How to use combineLatest for two public subjects in RXSwfit - ios

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)
}

Related

Make a network call every 10 seconds with RxSwift

I am completely new to RxSwift. I managed to load my table view but now I would like to make a call every 10 seconds.
I was reading here that I should probably use Observable<Int>.interval(10, scheduler: MainScheduler.instance), I tried without much success.
class MarketService: MarketServiceProtocol {
func fetchMarkets() -> Observable <[Market]> {
return Observable.create { observer -> Disposable in
RestManager.shared.makeRequest(withEndPoint: "market/v2/get-summary?region=US" , withHttpMethod: .get) { result in
if let error = result.error {
observer.onError(error)
return
}
guard let response = result.response,
200 ... 299 ~= response.httpStatusCode else {
return
}
guard let data = result.data else {
return
}
do {
let decodedData = try JSONDecoder().decode(MarketResult.self, from: data)
observer.onNext(decodedData.marketSummaryAndSparkResponse.markets)
} catch {
observer.onError(error)
}
}
return Disposables.create { }
}
}
}
then I call in my view controller:
viewModel.fetchMarketViewModels().observe(on: MainScheduler.instance).bind(to: tableView.rx.items(cellIdentifier: HomeTableViewCell.cellIdentifier)) {
index, viewModel, cell in
guard let cell = cell as? HomeTableViewCell else { return }
cell.setupData(viewModel: viewModel)
}.disposed(by: self.disposableBag)
There are a couple of problems with your Observable.create closure. You have to make sure that something is sent to the observer in every path, otherwise the Observable will call the function and then not emit anything and you will not know why.
Also, you want to minimize the amount of logic being performed in the closure passed to create. You are doing way too much in there.
So let's simplify the code in the create closure as much as possible first:
extension RestManager {
func rx_makeRequest(withEndPoint endPoint: String, withHttpMethod method: HttpMethod) -> Observable<(response: MyHTTPURLResponse, data: Data)> {
Observable.create { observer in
self.makeRequest(withEndPoint: endPoint, withHttpMethod: method) { result in
if let response = result.response, let data = result.data {
observer.onNext((response, data))
observer.onCompleted()
}
else {
observer.onError(result.error ?? RxError.unknown)
}
}
return Disposables.create() // is there some way of canceling a request? If so, it should be done here.
}
}
}
This does the bare minimum. Just wraps the underlying callback and nothing else. Now your fetchMarkets call is much simpler:
class MarketService: MarketServiceProtocol {
func fetchMarkets() -> Observable <[Market]> {
return RestManager.shared.rx_makeRequest(withEndPoint: "market/v2/get-summary?region=US", withHttpMethod: .get)
.do(onNext: { result in
guard 200...299 ~= result.response.httpStatusCode
else { throw URLError.httpRequestFailed(response: result.response, data: result.data) }
})
.map { try JSONDecoder().decode(MarketResult.self, from: $0.data).marketSummaryAndSparkResponse.markets }
}
}
Now to the meat of your question. How to make the network call every 10 seconds... Just wrap your network call in a flatMap like this:
Observable<Int>.interval(.seconds(10), scheduler: MainScheduler.instance)
.flatMapLatest { _ in
viewModel.fetchMarketViewModels()
}
.observe(on: MainScheduler.instance)
.bind(to: tableView.rx.items(cellIdentifier: HomeTableViewCell.cellIdentifier)) { index, viewModel, cell in
guard let cell = cell as? HomeTableViewCell else { return }
cell.setupData(viewModel: viewModel)
}
.disposed(by: self.disposableBag)
Learn more about flatMap and its variants from this article.

Swift/iOS - How to use a value from one scope/function and pass it into another?

I am trying to pass the value of gyroX to another function but it just ends up in it having a value of 0 when I use it as gyroX in that other function.
Here is the code:
var gyroX = Float()
motion.startGyroUpdates(to: .main) { (data, error) in
if let myData = data {
gyroX = Float(myData.rotationRate.x)
}
}
With Xcode 13 Beta and Swift 5.5
This is a problem that we can now solve with Async/Await's Continuations
We would first make a function that converts the callback into an awaitable result like:
func getXRotation(from motion: CMMotionManager) async throws -> Float {
try await withCheckedThrowingContinuation { continuation in
class GyroUpdateFailure: Error {} // make error to throw
motion.startGyroUpdates(to: .main) { (data, error) in
if let myData = data {
continuation.resume(returning: Float(myData.rotationRate.x))
} else {
throw GyroUpdateFailure()
}
}
}
}
Then we can assign the variable and use it like so:
let gyroX = try await getXRotation(from: motion)
callSomeOtherFunction(with: gyroX)
With Xcode <= 12 and Combine
In the current release of Swift and Xcode we can use the Combine framework to make callback handling a little easier for us. First we'll convert the closure from the motion manager into a "Future". Then we can use that future in a combine chain.
func getXRotation(from motion: CMMotionManager) -> Future<CMGyroData, Error> {
Future { promise in
class GyroUpdateFailure: Error {} // make error to throw
motion.startGyroUpdates(to: .main) { (data, error) in
if let myData = data {
promise(.success(myData))
} else {
promise(.failure(GyroUpdateFailure()))
}
}
}
}
// This is the other function you want to call
func someOtherFunction(_ x: Float) {}
// Then we can use it like so
_ = getXRotation(from: motion)
.eraseToAnyPublisher()
.map { Float($0.rotationRate.x) }
.map(someOtherFunction)
.sink { completion in
switch completion {
case .failure(let error):
print(error.localizedDescription)
default: break
}
} receiveValue: {
print($0)
}
There are some important parts to the combine flow. The _ = is one of them. The result of "sinking" on a publisher is a "cancellable" object. If we don't store that in a local variable the system can clean up the task before it fishes executing. So you will want to do that for sure.
I highly recommend you checkout SwiftBySundell.com to learn more about Combine or Async/Await and RayWenderlich.com for mobile development in general.

Swift Combine: `append` which does not require output to be equal?

Using Apple's Combine I would like to append a publisher bar after a first publisher foo has finished (ok to constrain Failure to Never). Basically I want RxJava's andThen.
I have something like this:
let foo: AnyPublisher<Fruit, Never> = /* actual publisher irrelevant */
let bar: AnyPublisher<Fruit, Never> = /* actual publisher irrelevant */
// A want to do concatenate `bar` to start producing elements
// only after `foo` has `finished`, and let's say I only care about the
// first element of `foo`.
let fooThenBar = foo.first()
.ignoreOutput()
.append(bar) // Compilation error: `Cannot convert value of type 'AnyPublisher<Fruit, Never>' to expected argument type 'Publishers.IgnoreOutput<Upstream>.Output' (aka 'Never')`
I've come up with a solution, I think it works, but it looks very ugly/overly complicated.
let fooThenBar = foo.first()
.ignoreOutput()
.flatMap { _ in Empty<Fruit, Never>() }
.append(bar)
I'm I missing something here?
Edit
Added a nicer version of my initial proposal as an answer below. Big thanks to #RobNapier!
I think instead of ignoreOutput, you just want to filter all the items, and then append:
let fooThenBar = foo.first()
.filter { _ in false }
.append(bar)
You may find this nicer to rename dropAll():
extension Publisher {
func dropAll() -> Publishers.Filter<Self> { filter { _ in false } }
}
let fooThenBar = foo.first()
.dropAll()
.append(bar)
The underlying issue is that ignoreAll() generates a Publisher with Output of Never, which usually makes sense. But in this case you want to just get ride of values without changing the type, and that's filtering.
Thanks to great discussions with #RobNapier we kind of concluded that a flatMap { Empty }.append(otherPublisher) solution is the best when the output of the two publishers differ. Since I wanted to use this after the first/base/'foo' publisher finishes, I've written an extension on Publishers.IgnoreOutput, the result is this:
Solution
protocol BaseForAndThen {}
extension Publishers.IgnoreOutput: BaseForAndThen {}
extension Combine.Future: BaseForAndThen {}
extension Publisher where Self: BaseForAndThen, Self.Failure == Never {
func andThen<Then>(_ thenPublisher: Then) -> AnyPublisher<Then.Output, Never> where Then: Publisher, Then.Failure == Failure {
return
flatMap { _ in Empty<Then.Output, Never>(completeImmediately: true) } // same as `init()`
.append(thenPublisher)
.eraseToAnyPublisher()
}
}
Usage
In my use case I wanted to control/have insight in when the base publisher finishes, therefore my solution is based on this.
Together with ignoreOutput
Since the second publisher, in case below appleSubject, won't start producing elements (outputting values) until the first publisher finishes, I use first() operator (there is also a last() operator) to make the bananaSubject finish after one output.
bananaSubject.first().ignoreOutput().andThen(appleSubject)
Together with Future
A Future already just produces one element and then finishes.
futureBanana.andThen(applePublisher)
Test
Here is the complete unit test (also on Github)
import XCTest
import Combine
protocol Fruit {
var price: Int { get }
}
typealias 🍌 = Banana
struct Banana: Fruit {
let price: Int
}
typealias 🍏 = Apple
struct Apple: Fruit {
let price: Int
}
final class CombineAppendDifferentOutputTests: XCTestCase {
override func setUp() {
super.setUp()
continueAfterFailure = false
}
func testFirst() throws {
try doTest { bananaPublisher, applePublisher in
bananaPublisher.first().ignoreOutput().andThen(applePublisher)
}
}
func testFuture() throws {
var cancellable: Cancellable?
try doTest { bananaPublisher, applePublisher in
let futureBanana = Future<🍌, Never> { promise in
cancellable = bananaPublisher.sink(
receiveCompletion: { _ in },
receiveValue: { value in promise(.success(value)) }
)
}
return futureBanana.andThen(applePublisher)
}
XCTAssertNotNil(cancellable)
}
static var allTests = [
("testFirst", testFirst),
("testFuture", testFuture),
]
}
private extension CombineAppendDifferentOutputTests {
func doTest(_ line: UInt = #line, _ fooThenBarMethod: (AnyPublisher<🍌, Never>, AnyPublisher<🍏, Never>) -> AnyPublisher<🍏, Never>) throws {
// GIVEN
// Two publishers `foo` (🍌) and `bar` (🍏)
let bananaSubject = PassthroughSubject<Banana, Never>()
let appleSubject = PassthroughSubject<Apple, Never>()
var outputtedFruits = [Fruit]()
let expectation = XCTestExpectation(description: self.debugDescription)
let cancellable = fooThenBarMethod(
bananaSubject.eraseToAnyPublisher(),
appleSubject.eraseToAnyPublisher()
)
.sink(
receiveCompletion: { _ in expectation.fulfill() },
receiveValue: { outputtedFruits.append($0 as Fruit) }
)
// WHEN
// a send apples and bananas to the respective subjects and a `finish` completion to `appleSubject` (`bar`)
appleSubject.send(🍏(price: 1))
bananaSubject.send(🍌(price: 2))
appleSubject.send(🍏(price: 3))
bananaSubject.send(🍌(price: 4))
appleSubject.send(🍏(price: 5))
appleSubject.send(completion: .finished)
wait(for: [expectation], timeout: 0.1)
// THEN
// A: I the output contains no banana (since the bananaSubject publisher's output is ignored)
// and
// B: Exactly two apples, more specifically the two last, since when the first Apple (with price 1) is sent, we have not yet received the first (needed and triggering) banana.
let expectedFruitCount = 2
XCTAssertEqual(outputtedFruits.count, expectedFruitCount, line: line)
XCTAssertTrue(outputtedFruits.allSatisfy({ $0 is 🍏 }), line: line)
let apples = outputtedFruits.compactMap { $0 as? 🍏 }
XCTAssertEqual(apples.count, expectedFruitCount, line: line)
let firstApple = try XCTUnwrap(apples.first)
let lastApple = try XCTUnwrap(apples.last)
XCTAssertEqual(firstApple.price, 3, line: line)
XCTAssertEqual(lastApple.price, 5, line: line)
XCTAssertNotNil(cancellable, line: line)
}
}
As long as you use .ignoreOutput(), it is safe to replace "ugly" .flatMap { _ in Empty<Fruit, Never>() } to simple .map { Fruit?.none! } which will never be called anyway and just changes the Output type.

Rx Observable that gets value from other Observable

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.

RxSwift: code working only first time

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)

Resources