Making network requests more reactive with RxMoya.
The Rx ecosystem is strong. Before RxSwift, I used a brittle wrapper of an "Observable" type in my codebases, just so I could "subscribe" to values. It was really helpful to start, as I personally saw the benefits of being able to bind value changes to your UI, or any side effect really.
It worked to begin with, but then I realized:
- My wrapper had 0 tests
- My "Observable" lacked access control features. anything could observe and push values to an observable stream.
- RxSwift is way more popular and does what I want... times 1000
So if you're like me and you've added reactive paradigms like RxSwift to your codebase to slowly introduce yourself to Observables, you're probably still confused. Because the learning curve is STEEP.
Fortunately, I spend without thinking on educational resources like raywenderlich.com and Egghead.io, where there's plenty on content about Rx in iOS, Android and JavaScript.
I've written briefly about Moya, networking library that sits on top of Alamofire, and how to get started with the library way back. (It's actually my most viewed blog post, yet the tutorial is so outdated, sorry.)
It's a great library, and aside from some desolate areas in SuperFit iOS where URLSession is used here and there, Moya does a really great job with extending a codebase's networking requirements from various endpoints, or "providers" in Moya speak.
One big mistake I've been making with having both Moya and RxSwift in my codebase is that I'm manually wrapping Moya requests and operations inside an Observable.
In the code block below, I am making a network request to a remote resource that returns "Sections"; With the sections data, I want to persist the models, as well as append any client-side sections, such as a tool-tip, to my list before ultimately handing it off to a ViewController to render.
self.listSections = fetchCatalogSectionsProperty.asObservable()
.flatMap { (_) -> RxSwift.Observable<[ListDiffable]> in
return RxSwift.Observable<[ListDiffable]>.create({ observer -> RxSwift.Disposable in
programCatalogManager.fetchAndCacheCatalogSections(liveOnly: !AppEnvironment.current.authorizationManager.isAdmin, { (result) in
switch result {
case .success(let sections):
var sectionsToShow = [ListDiffable]()
// add catalog sections
let orderedCatalogSections = Array(sections).sorted(by: {$0.title < $1.title})
sectionsToShow.append(contentsOf: orderedCatalogSections)
observer.onNext(sectionsToShow)
return
case .failure(let error):
throw error
}
})
return Disposables.create()
})
}
I used to have a sensitive instinct to drying up code or refactoring code smells, but more recently, I accepted that I cannot spend too much time figuring out the better way to do something on lines of code that really don't matter in the grand scheme of a business.
However, regarding this code block, and the 10 others places in my code where I manually encapsulate network requests in observables, it was finally time to consider a smarter approach and figure out how I could make the networking layer more "reactive".
And. yeah, the answer is literally in Moya's repo. They have Rx extensions you can use, aliased as "RxMoya". Instead of pod Moya
in your Podfile, simply change that to pod 'Moya/RxSwift'
.
This is what my code block looks like now using observable network requests with RxMoya.
func fetchAndCacheCatalogSections(liveOnly: Bool)-> Single<[CatalogSection]> {
let endpoint: CatalogSectionService = liveOnly ? .getLiveOnlyCatalogSections : .getAllCatalogSections
return catalogSectionService.rx.request(endpoint)
.map { response -> [CatalogSection] in
do {
// Core Data Stuff here
return sections
} catch let error {
AppEnvironment.current.logger.print(error.localizedDescription)
throw CatalogManagerError.errorDecoding(CatalogSectionRemote.self)
}
}
}
And what the caller of fetchAndCacheCatalogSections
looks like:
self.listSections = fetchCatalogSectionsProperty
.flatMap({ (_) -> Observable<[CatalogSection]> in
return programCatalogManager.fetchAndCacheCatalogSections(liveOnly: !AppEnvironment.current.authorizationManager.isAdmin).asObservable()
})
.map({ (sections) -> [ListDiffable] in
var sectionsToShow = [ListDiffable]()
// add catalog sections
let orderedCatalogSections = Array(sections).sorted(by: {$0.title < $1.title})
sectionsToShow.append(contentsOf: orderedCatalogSections)
return sectionsToShow
})
.asDriver(onErrorRecover: { (error) in
print(error.localizedDescription)
let request = AppEnvironment.current.coreDataStack.fetch(request: CatalogSection.defaultFetchRequest())
guard let cachedSections = request.result else {
return Driver.just([])
}
let sortedSections = Array(cachedSections).sorted(by: {$0.title < $1.title})
return Driver.just(sortedSections)
})
Instead of fetchAndCacheCatalogSections
calling back with the actual response, I now begin with a request Observable from RxMoya, and I perform response data parsing and transformations in a map against the observable. Finally, CatalogSectionListViewModel
, the struct that invokes fetchAndCacheCatalogSections
maps the observable once more to attach any additional UI sections beyond the data from the network, then returns a `Driver` (an observable with specific traits), which my view controllers can then consume.
Using RxMoya freed me up from having to ever create an Observable type myself. (Of course, creating the observable needs to happen somewhere; in this case; RxMoya is doing that in its extensions, but I don't see it, which is all that matters~).
I'm clearly able to fetch my data, map my data, and centralize error handling with observables and some Rx goodies like Driver
and Single
.
In the end, all my view controller needs to do is subscribe and bind to its viewmodel's list observable and render a collection view.
catalogSectionListViewModel.outputs.listSections.drive(onNext: { [weak self] sections in
self?.uiSections = sections
self?.adapter.performUpdates(animated: true)
self?.feedRefresh.endRefreshing(updates: nil, completion: nil)
}).disposed(by: bag)
For all the IGListKit fans out there, you may be thinking... there's are Rx extensions for IGListKit as well. Until then, progress is better than nothing— I'll get there when I get there!