Escape callback hell when using firebase functions - ios

I'm familiar with JavaScript promises, but I'm new to swift and Firebase, and I don't have anyone to ask on my team. I've tried researching different ways of handling async operations without callback hell, but I can't understand how to make it work with firebase functions. Right now I'm using a really complicated mess of DispatchGroups and callbacks to make the code somewhat work, but I really want to make it cleaner and more maintainable.
My code looks something like this (error handling removed for conciseness):
var array = []
let dispatch = DispatchGroup()
db.collection("documentA").getDocuments() { (querySnapshot, err) in
for document in querySnapshot.documents
dispatch.enter()
let dataA = document.data()["dataA"]
...
db.collection("documentB").documents(dataA).getDocuments() { (document, error) in
let dataB = document.data()["dataB"]
...
db.collection("documentC").documents(dataB).getDocuments() { (document, error) in
let dataC = document.data()["dataC"]
let newObject = NewObject(dataA,dataB,dataC)
self.array.append(newObject)
dispatch.leave()
}
}
}
//Use dispatch group to notify main queue to update tableView using contents of this array
Does anyone have any recommended learning resources or advice on how I can tackle this problem?

I recommend you consider bringing in the RxFirebase library. Rx is a great way to clean up nested closures (callback hell).
In looking over your sample code, the first thing you have to understand is that only so much can be done. The problem itself has a lot of essential complexity. Also, there's a lot going on in this code that can be broken out. Once you do that, you can boil down the problem to the following:
import Curry // the Curry library is in Cocoapods
func example(db: Firestore) -> Observable<[NewObject]> {
let getObjects = curry(getData(db:collectionId:documentId:))(db)
let xs = getObjects("documentA")("dataA")
let xys = xs.flatMap { parentsAndChildren(fn: getObjects("documentB"), parent: { $0 }, xs: $0) }
let xyzs = xys.flatMap { parentsAndChildren(fn: getObjects("documentC"), parent: { $0.1 }, xs: $0) }
return xyzs.mapT { NewObject(dataA: $0.0.0, dataB: $0.0.1, dataC: $0.1) }
}
Note that this is making extensive use of higher order functions, so understanding those will help a lot. If you don't want to use higher order functions, you could use classes instead, but the amount of code you would have to write would at least double and you would likely have problems with memory cycles.
To make the above so simple requires some support code:
func getData(db: Firestore, collectionId: String, documentId: String) -> Observable<[String]> {
return db.collection(collectionId).rx.getDocuments()
.map { getData(documentId: documentId, snapshot: $0) }
}
func parentsAndChildren<X>(fn: (String) -> Observable<[String]>, parent: (X) -> String, xs: [X]) -> Observable<[(X, String)]> {
Observable.combineLatest(xs.map { x in
fn(parent(x)).map { apply(x: x, ys: $0) }
})
.map { $0.flatMap { $0 } }
}
extension ObservableType {
func mapT<T, U>(_ transform: #escaping (T) -> U) -> Observable<[U]> where Element == [T] {
map { $0.map(transform) }
}
}
The getData(db:collectionId:documentId:) function asks for the strings in the collection associated with the document.
The parentsAndChildren(fn:parent:xs:) function is probably the most complex. It will extract the appropriate parent object from the generic X type, get the children from the server and roll them up into a single dimensional array of parents and children. For example if the parents are ["a", "b"], the children of "a" are ["w", "x"] and the children of "b" are ["y", "z"], then the function will output [("a", "w"), ("a", "x"), ("b", "y"), ("b", "z")] (contained in an Observable.)
The Observable.mapT(_:) function allows us to map through an Observable Array of objects and do something to them. Of course, you could just do xyzs.map { $0.map { NewObject(dataA: $0.0.0, dataB: $0.0.1, dataC: $0.1) } }, but I feel this is cleaner.
Here is the support code for the above functions:
extension Reactive where Base: CollectionReference {
func getDocuments() -> Observable<QuerySnapshot> {
Observable.create { [base] observer in
base.getDocuments { snapshot, error in
if let snapshot = snapshot {
observer.onNext(snapshot)
observer.onCompleted()
}
else {
observer.onError(error ?? RxError.unknown)
}
}
return Disposables.create()
}
}
}
func getData(documentId: String, snapshot: QuerySnapshot) -> [String] {
snapshot.documents.compactMap { $0.data()[documentId] as? String }
}
func apply<X>(x: X, ys: [String]) -> [(X, String)] {
ys.map { (x, $0) }
}
The Reactive.getDocuments() function actually makes the firebase request. Its job is to turn the callback closure into an object so that you can deal with it easier. This is the piece that RxFirebase should give you, but as you can see, it's pretty easy to write it on your own.
The getData(documentId:snapshot:) function just extracts the appropriate data out of the snapshot.
The app(x:ys:) function is what keeps the whole thing in a single dimensional array by copying the X for each child.
Lastly, notice that most of the functions above are easily and independently unit testable and the ones that aren't are exceptionally simple...

Related

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.

Best way to call multiple API requests in a for loop in RxSwift

I have to make several api calls (approx 100) using a for loop and on completion of this I need to complete the Observable. I am using it as following:
func getMaterialInfo(materialNo:[String]) -> Observable<[String: Material]>{
return Observable.create({ (observable) -> Disposable in
for (index,mat) in materialNo.enumerated(){
// Pass the material number one by one to get the Material object
self.getMaterialInfo(materialNo: mat).subscribe(onNext: { material in
var materialDict: [String: Material] = [:]
materialDict[material.materialNumber] = material
observable.onNext(materialDict)
if index == (materialNo.count-1){
observable.onCompleted()
}
}, onError: { (error) in
observable.onError(error)
}, onCompleted: {
}).disposed(by: self.disposeBag)
}
return Disposables.create()
})
}
Although loop is working fine and observable.onCompleted() is called but the caller method does not receive it.
I am calling it like following:
private func getImage(materialNo:[String]){
if materialNo.isEmpty {
return
}
var dictMaterials = [String:String]()
materialService.getMaterialInfo(materialNo: materialNo).subscribe(onNext: { (materials) in
for (key,value) in materials{
if (value.imageUrl != nil){
dictMaterials[key] = value.imageUrl
}
}
}, onError: { (error) in
}, onCompleted: {
self.view?.updateToolImage(toolImageList: dictMaterials)
}, onDisposed: {}).disposed(by: disposeBag)
}
OnCompleted block of Rx is not executing. How can I fix it?
Edit (5-March)
I revisited this answer, because I'm not sure what my brain was doing when I wrote the code sample below. I'd do something like this instead:
func getMaterialInfo(materialNo: String) -> Observable<[String: Material]> {
// ...
}
func getMaterialInfo(materialNumbers:[String]) -> Observable<[String: Material]>{
let allObservables = materialNumbers
.map { getMaterialInfo(materialNo: $0) }
return Observable.merge(allObservables)
}
Original answer
From your code, I interpret that all individual getMaterialInfo calls are done concurrently. Based on that, I would rewrite your getMaterialInfo(:[_]) method to use the .merge operator.
func getMaterialInfo(materialNo:[String]) -> Observable<[String: Material]>{
return Observable.create({ (observable) -> Disposable in
// a collection of observables that we haven't yet subscribed to
let allObservables = materialNo
.map { getMaterialInfo(materialNo: $0) }
return Observable.merge(allObservables)
}
return Disposables.create()
}
Note that using merge subscribes to all observable simultaneously, triggering 100 network requests at the same time. For sequential subscription, use concat instead!

Using parameters on async call in swift

I'm having an async call with a completionhandler that fetches data for me through a query. These queries can vary based upon the users action.
My data call looks like this;
class DataManager {
func requestVideoData(query: QueryOn<VideoModel>, completion: #escaping (([VideoModel]?, UInt?, Error?) -> Void)) {
client.fetchMappedEntries(matching: query) { (result: Result<MappedArrayResponse<FRVideoModel>>) in
completion(videos, arrayLenght, nil)
}
}
}
My ViewController looks like this;
DataManager().requestVideoData(query: /*One of the queries below*/) { videos, arrayLength, error in
//Use the data fetched based on the query that has been entered
}
My queries look like this;
let latestVideosQuery = QueryOn<FRVideoModel>().limit(to: 50)
try! latestVideosQuery.order(by: Ordering(sys: .createdAt, inReverse: true))
And this;
let countryQuery = QueryOn<FRVideoModel>()
.where(valueAtKeyPath: "fields.country.sys.contentType.sys.id", .equals("country"))
.where(valueAtKeyPath: "fields.country.fields.countryTitle", .includes(["France"]))
.limit(to: 50)
But I'm not completely sure how I would implement these queries the right way so they correspond with the MVC model.
I was thinking about a switch statement in the DataManager class, and pass a value into the query parameter on my ViewController that would result in the right call on fetchMappedEntries(). The only problem with this is that I still need to execute the correct function according to my query in my VC, so I would need a switch statement over there as well.
Or do I need to include all my queries inside my ViewController? This is something I think is incorrect because it seems something that should be in my model.
This is somewhat subjective. I think you are right to want to put the construction of the queries in your DataManager and not in your view controller.
One approach is to dumb down the request interface, so that the view controller only needs to pass a simple request, say:
struct QueryParams {
let limit: Int?
let country: String?
}
You would then need to change your DataManager query function to take this instead:
func requestVideoData(query: QueryParams, completion: #escaping (([VideoModel]?, UInt?, Error?) -> Void))
Again, this is subjective, so you have to determine the tradeoffs. Dumbing down the interface limits the flexibility of it, but it also simplifies what the view controller has to know.
In the end I went with a slightly modified networking layer and a router where the queries are stored in a public enum, which I can then use in my functions inside my ViewController. Looks something like this;
public enum Api {
case latestVideos(limit: UInt)
case countryVideos(countries: [String], limit: UInt)
}
extension Api:Query {
var query: QueryOn<VideoModel> {
switch self {
case .latestVideos(let limit):
let latestVideosQuery = QueryOn<VideoModel>().limit(to: limit)
try! latestVideosQuery.order(by: Ordering(sys: .createdAt, inReverse: true))
return latestVideosQuery
case .countryVideos(let countries, let limit):
let countryQuery = QueryOn<VideoModel>()
.where(valueAtKeyPath: "fields.country.sys.contentType.sys.id", .equals("country"))
.where(valueAtKeyPath: "fields.country.fields.countryTitle", .includes(countries))
.limit(to: limit)
return countryQuery
}
}
}
And the NetworkManager struct to fetch the data;
struct NetworkManager {
private let router = Router<Api>()
func fetchLatestVideos(limit: UInt, completion: #escaping(_ videos: [VideoModel]?, _ arrayLength: UInt?,_ error: Error?) -> Void) {
router.requestVideoData(.latestVideos(limit: limit)) { (videos, arrayLength, error) in
if error != nil {
completion(nil, nil, error)
} else {
completion(videos, arrayLength, nil)
}
}
}
}

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.

Resources