I'm trying to practice async/await with PromiseKit !
Here is my original function
func getProductDetailsList(stationId: String?) {
guard let productID = productID else {
return
}
getProductDetails(productID: productID)
.done { [weak self] products -> Void in
guard let firstProduct = products.first else {
throw DetailsEmptyError()
}
product?.model.isAvailable = true
self?.delegate?.didSucceedLoadingDetails()
}
.ensure {
self.delegate?.didEnsureLoadingDetails()
}
.catch { [weak self] _ in
self?.delegate?.didFailLoadingDetails()
}
}
And here how I tried to introduce async/await
func getProductDetailsList(stationId: String?) {
guard let productID = productID else {
return
}
let products = await getProductsDetails(productsIDs: productID)
guard let firstProduct = products.first else {
throw DetailsEmptyError()
}
product?.model.isAvailable = true
self?.delegate?.didSucceedLoadingDetails()
.ensure {
self.delegate?.didEnsureLoadingDetails()
}
.catch { [weak self] _ in
self?.delegate?.didFailLoadingDetails()
}
}
and here is my getProductsDetails function
func getProductsDetails(productsIDs: productID) async -> Promise<[ProductDetails?]> {
let promises = Set(productsIDs.compactMap { $0 })
.map { ProductDetails(id: $0) }
return when(resolved: promises)
.map { _ in
return productsIDs.map { productsID in promises.first(where: { $0.field?.id == productsID })?.field }
}
}
I don't know if this is the right path to follow in order to integrate async/await into an existing code + How can deal with the part of .ensure and .catch ?
Thanks !
How to get an error response with driver so I can show it in alert. When I see the trait driver is can't error out, so should I use subject or behaviourRelay to get error response when I subscribe. Actually I like how to use driver but I don't know how to passed error response using driver.
this is my network service
func getMovies(page: Int) -> Observable<[MovieItem]> {
return Observable.create { observer -> Disposable in
self.service.request(endpoint: .discover(page: page)) { data, response, error in
if let _ = error {
observer.onError(MDBError.unableToComplete)
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
observer.onError(MDBError.invalidResponse)
return
}
guard let data = data else {
observer.onError(MDBError.invalidData)
return
}
if let decode = self.decode(jsonData: MovieResults.self, from: data) {
observer.onNext(decode.results)
}
observer.onCompleted()
}
return Disposables.create()
}
}
This is my viewModel
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
class PopularViewModel: ViewModelType {
struct Input {
let viewDidLoad: Driver<Void>
}
struct Output {
let loading: Driver<Bool>
let movies: Driver<[MovieItem]>
}
private let service: NetworkDataFetcher
init(service: NetworkDataFetcher = NetworkDataFetcher(service: NetworkService())) {
self.service = service
}
func transform(input: Input) -> Output {
let loading = ActivityIndicator()
let movies = input.viewDidLoad
.flatMap { _ in
self.service.getMovies(page: 1)
.trackActivity(loading)
.asDriver(onErrorJustReturn: [])
}
let errorResponse = movies
return Output(loading: loading.asDriver(),movies: movies)
}
}
this is how I bind the viewModel in viewController
let input = PopularViewModel.Input(viewDidLoad: rx.viewDidLoad.asDriver())
let output = viewModel.transform(input: input)
output.movies.drive { [weak self] movies in
guard let self = self else { return }
self.populars = movies
self.updateData(on: movies)
}.disposed(by: disposeBag)
output.loading
.drive(UIApplication.shared.rx.isNetworkActivityIndicatorVisible)
.disposed(by: disposeBag)
You do this the same way you handled the ActivityIndicator...
The ErrorRouter type below can be found here.
This is such a common pattern that I have created an API class that takes care of this automatically.
class PopularViewModel: ViewModelType {
struct Input {
let viewDidLoad: Driver<Void>
}
struct Output {
let loading: Driver<Bool>
let movies: Driver<[MovieItem]>
let displayAlertMessage: Driver<String>
}
private let service: NetworkDataFetcher
init(service: NetworkDataFetcher = NetworkDataFetcher(service: NetworkService())) {
self.service = service
}
func transform(input: Input) -> Output {
let loading = ActivityIndicator()
let errorRouter = ErrorRouter()
let movies = input.viewDidLoad
.flatMap { [service] in
service.getMovies(page: 1)
.trackActivity(loading)
.rerouteError(errorRouter)
.asDriver(onErrorRecover: { _ in fatalError() })
}
let displayAlertMessage = errorRouter.error
.map { $0.localizedDescription }
.asDriver(onErrorRecover: { _ in fatalError() })
return Output(
loading: loading.isActive.asDriver(onErrorRecover: { _ in fatalError() }),
movies: movies,
displayAlertMessage: displayAlertMessage
)
}
}
I have two snapshot listeners and I need to run them in same completion block to get data to same array on first time when application starts. After application is started and listeners are listening I need to run functions separately. I cannot use completion block because if data changes on fetchOwnGames function it also calls another fetchFriendsGames function.
func fetchData(completion: #escaping () -> Void) {
if games.count == 0 {
self.fetchOwnGames {
self.fetchFriendsGames {
completion()
}
}
}
}
Also I cannot use dispatchGroup because if function completion called dispatchGroup.leave() function is getting error
func fetchData(completion: #escaping () -> Void) {
if games.count == 0 {
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
fetchOwnGames {
dispatchGroup.leave()
}
dispatchGroup.enter()
fetchFriendsGames {
dispatchGroup.leave()
}
dispatchGroup.notify(queue: DispatchQueue.main) {
completion()
}
}
}
How I can call functions separately but data comes same time.
Here is my fetchOwnGames and fetchFriendGames functions
func fetchOwnGames(completion: #escaping () -> Void) {
guard let ownUid = UserService.shared.currentUser?.id else { return }
ownListener = Constants.FirebaseCollection.gamesCollection
.order(by: "startTime")
.whereField("ownerUid", isEqualTo: ownUid)
.limit(toLast: 5)
.addSnapshotListener { [self] querySnapshot, error in
guard let querySnapshot = querySnapshot, error == nil else {
print("DEBUG: error", error?.localizedDescription as Any)
return
}
querySnapshot.documentChanges.forEach { (change) in
switch change.type {
case .added:
guard let data = try? change.document.data(as: Game.self) else { return }
self.games.append(data)
self.games = games.sorted(by: { $0.endTime.compare($1.endTime) == .orderedDescending})
self.ownGames.append(data)
SettingsManager.shared.gamesCount = ownGames.count
case .modified:
guard let data = try? change.document.data(as: Game.self) else { return }
if let index = self.games.firstIndex(where: { $0.courseId == data.courseId }) {
self.games[index] = data
}
case .removed:
guard let data = try? change.document.data(as: Game.self) else { return }
self.games = self.games.filter { $0 != data }
self.games = games.sorted(by: { $0.endTime.compare($1.endTime) == .orderedDescending})
SettingsManager.shared.gamesCount = games.count
}
}
print("DEBUG2: owngames count", ownGames.count)
completion()
}
}
func fetchFriendsGames(completion: #escaping () -> Void) {
userService.fetchFriends(friendCompletion: { [self] friends in
let friendsUidArray = friends.map { $0.id }
if friendsUidArray.count == 0 {
completion()
} else {
for uid in friendsUidArray {
guard let uid = uid else { return }
friendListener = Constants.FirebaseCollection.gamesCollection
.order(by: "startTime", descending: true)
.whereField("ownerUid", isEqualTo: uid)
.limit(to: 5)
.addSnapshotListener({ querySnapshot, error in
guard let querySnapshot = querySnapshot, error == nil else {
print("DEBUG: error", error?.localizedDescription as Any)
return
}
querySnapshot.documentChanges.forEach { change in
switch change.type {
case .added:
guard let data = try? change.document.data(as: Game.self) else { return }
self.games.append(data)
self.friendGames.append(data)
self.games = games.sorted(by: { $0.startTime.compare($1.startTime) == .orderedDescending})
case .modified:
guard let data = try? change.document.data(as: Game.self) else { return }
if let index = self.games.firstIndex(where: { $0.courseId == data.courseId }) {
self.games[index] = data
}
case .removed:
guard let data = try? change.document.data(as: Game.self) else { return }
self.games = self.games.filter { $0 != data }
self.games = games.sorted(by: { $0.endTime.compare($1.endTime) == .orderedDescending})
}
}
print("DEBUG3: friendgames count", friendGames.count)
completion()
})
}
}
})
}
Got it work with adding boolean checker.
private var appStarted = false
func fetchData(completion: #escaping () -> Void) {
if games.count == 0 {
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
fetchOwnGames {
if !self.appStarted {
dispatchGroup.leave()
}
}
dispatchGroup.enter()
fetchFriendsGames {
if !self.appStarted {
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: DispatchQueue.main) {
self.appStarted = true
completion()
}
}
}
Currently I have services that manages retrieving data from the local storage, but also checks the remote network for any modified data. It is using a completion handler with Result pattern and protocol type, but would like to convert this to an observable approach.
Here is the current logic:
struct AuthorWorker: AuthorWorkerType, Loggable {
private let store: AuthorStore
private let remote: AuthorRemote
init(store: AuthorStore, remote: AuthorRemote) {
self.store = store
self.remote = remote
}
}
extension AuthorWorker {
func fetch(id: Int, completion: #escaping (Result<AuthorType, DataError>) -> Void) {
store.fetch(id: id) {
// Immediately return local response
completion($0)
guard case .success(let cacheElement) = $0 else { return }
// Sync remote updates to cache if applicable
remote.fetch(id: id) {
// Validate if any updates occurred and return
guard case .success(let element) = $0,
element.modifiedAt > cacheElement.modifiedAt else {
return
}
// Update local storage with updated data
self.store.createOrUpdate(element) {
guard case .success = $0 else { return }
// Callback handler again if updated
completion($0)
}
}
}
}
}
I'm always instantly returning the local data to the UI so the user doesn't wait. In the background, it is checking the remote network for modified data and updates the UI again only if necessary. I use it like this:
authorWorker.fetch(1) { [weak self] in
guard case .success(let value) = $0 else {
// alert error
}
self?.myLabel.text = value.name
}
How can this be converted to RxSwift or an observable concept? This is what I got started, but I don't see the code on the walls like Neo yet when it comes to Rx, so I need help seeing the light.
extension AuthorWorker {
func fetch(id: Int) -> Observable<AuthorType> {
return Observable<AuthorType>.create { observer in
store.fetch(id: id) {
// Immediately return local response
observer.on(.next($0))
guard case .success(let cacheElement) = $0 else {
observer.on(.completed)
return
}
// Sync remote updates to cache if applicable
remote.fetch(id: id) {
// Validate if any updates occurred and return
guard case .success(let element) = $0,
element.modifiedAt > cacheElement.modifiedAt else {
observer.on(.completed)
return
}
// Update local storage with updated data
self.store.createOrUpdate(element) {
guard case .success = $0 else {
observer.on(.completed)
return
}
// Callback handler again if updated
observer.on(.next($0))
observer.on(.completed)
}
}
}
}
}
}
Then I would use it like this?
authorWorker.fetch(1).subscribe { [weak self] in
guard let element = $0.element else {
// Handle error how?
return
}
self?.myLabel.text = element.name
}
Is this the right approach or is there a more recommended way to do this? Is it also worth converting the underlying remote and local stores to observable as well, or does it make sense not to convert all things to observable all the time?
New Answer
Based on the comment, I see that you want something much more elaborate than my first answer, so here you go.
func worker<T: Equatable>(store: Observable<T>, remote: Observable<T>) -> (value: Observable<T>, store: Observable<T>) {
let sharedStore = store.share(replay: 1)
let sharedRemote = remote.share(replay: 1)
let value = Observable.merge(sharedStore, sharedRemote)
.distinctUntilChanged()
.takeUntil(sharedRemote.materialize().filter { $0.isStopEvent })
let store = Observable.zip(sharedStore, sharedRemote)
.filter { $0.0 != $0.1 }
.map { $0.1 }
return (value: value, store: store)
}
Here is the code above being used in your AuthorWorker class:
extension AuthorWorker {
func fetch(id: Int) -> Observable<AuthorType> {
let (_value, _store) = worker(store: store.fetch(id: id), remote: remote.fetch(id: id))
_ = _store
.subscribe(onNext: store.createOrUpdate)
return _value
}
}
And here is a test suite proving it works properly:
class Tests: XCTestCase {
var scheduler: TestScheduler!
var emission: TestableObserver<String>!
var storage: TestableObserver<String>!
var disposeBag: DisposeBag!
override func setUp() {
super.setUp()
scheduler = TestScheduler(initialClock: 0)
emission = scheduler.createObserver(String.self)
storage = scheduler.createObserver(String.self)
disposeBag = DisposeBag()
}
func testHappyPath() {
let storeProducer = scheduler.createColdObservable([.next(10, "store"), .completed(11)])
let remoteProducer = scheduler.createColdObservable([.next(20, "remote"), .completed(21)])
let (value, store) = worker(store: storeProducer.asObservable(), remote: remoteProducer.asObservable())
disposeBag.insert(
value.subscribe(emission),
store.subscribe(storage)
)
scheduler.start()
XCTAssertEqual(emission.events, [.next(10, "store"), .next(20, "remote"), .completed(21)])
XCTAssertEqual(storage.events, [.next(20, "remote"), .completed(21)])
}
func testSameValue() {
let storeProducer = scheduler.createColdObservable([.next(10, "store"), .completed(11)])
let remoteProducer = scheduler.createColdObservable([.next(20, "store"), .completed(21)])
let (value, store) = worker(store: storeProducer.asObservable(), remote: remoteProducer.asObservable())
disposeBag.insert(
value.subscribe(emission),
store.subscribe(storage)
)
scheduler.start()
XCTAssertEqual(emission.events, [.next(10, "store"), .completed(21)])
XCTAssertEqual(storage.events, [.completed(21)])
}
func testRemoteFirst() {
let storeProducer = scheduler.createColdObservable([.next(20, "store"), .completed(21)])
let remoteProducer = scheduler.createColdObservable([.next(10, "remote"), .completed(11)])
let (value, store) = worker(store: storeProducer.asObservable(), remote: remoteProducer.asObservable())
disposeBag.insert(
value.subscribe(emission),
store.subscribe(storage)
)
scheduler.start()
XCTAssertEqual(emission.events, [.next(10, "remote"), .completed(11)])
XCTAssertEqual(storage.events, [.next(20, "remote"), .completed(21)])
}
func testRemoteFirstSameValue() {
let storeProducer = scheduler.createColdObservable([.next(20, "store"), .completed(21)])
let remoteProducer = scheduler.createColdObservable([.next(10, "store"), .completed(11)])
let (value, store) = worker(store: storeProducer.asObservable(), remote: remoteProducer.asObservable())
disposeBag.insert(
value.subscribe(emission),
store.subscribe(storage)
)
scheduler.start()
XCTAssertEqual(emission.events, [.next(10, "store"), .completed(11)])
XCTAssertEqual(storage.events, [.completed(21)])
}
}
Previous Answer
I'd be inclined to aim for a usage like this:
let result = authorWorker.fetch(id: 1)
.share()
result
.map { $0.description }
.catchErrorJustReturn("")
.bind(to: myLabel.rx.text)
.disposed(by: disposeBag)
result
.subscribe(onError: { error in
// handle error here
})
.disposed(by: disposeBag)
The above can be accomplished if you have something like the below for example:
extension AuthorWorker {
func fetch(id: Int) -> Observable<AuthorType> {
return Observable.merge(store.fetch(id: id), remote.fetch(id: id))
.distinctUntilChanged()
}
}
extension AuthorStore {
func fetch(id: Int) -> Observable<AuthorType> {
return Observable.create { observer in
self.fetch(id: id, completion: { result in
switch result {
case .success(let value):
observer.onNext(value)
observer.onCompleted()
case .failure(let error):
observer.onError(error)
}
})
return Disposables.create()
}
}
}
extension AuthorRemote {
func fetch(id: Int) -> Observable<AuthorType> {
return Observable.create { observer in
self.fetch(id: id, completion: { result in
switch result {
case .success(let value):
observer.onNext(value)
observer.onCompleted()
case .failure(let error):
observer.onError(error)
}
})
return Disposables.create()
}
}
}
I am using RxSwift to make a pull to refresh and refreshNextPage.
Currently, here is the viewModel I work so far:
public final class MomentViewModel {
// Property
let refreshTrigger = PublishSubject<Void>()
let loadNextPageTrigger = PublishSubject<Void>()
let loading = Variable<Bool>(false)
let posts = Variable<[Post]>([])
var pageIndex: Int = 0
let error = PublishSubject<Swift.Error>()
private let disposeBag = DisposeBag()
public init() {
let refreshRequest = loading.asObservable()
.sample(refreshTrigger)
.flatMap { loading -> Observable<Int> in
if loading {
return Observable.empty()
} else {
return Observable<Int>.create { observer in
self.pageIndex = 0
print("reset page index to 0")
observer.onNext(0)
observer.onCompleted()
return Disposables.create()
}
}
}
.debug("refreshRequest", trimOutput: false)
let nextPageRequest = loading.asObservable()
.sample(loadNextPageTrigger)
.flatMap { loading -> Observable<Int> in
if loading {
return Observable.empty()
} else {
return Observable<Int>.create { [unowned self] observer in
self.pageIndex += 1
print(self.pageIndex)
observer.onNext(self.pageIndex)
observer.onCompleted()
return Disposables.create()
}
}
}
.debug("nextPageRequest", trimOutput: false)
let request = Observable.merge(refreshRequest, nextPageRequest)
.debug("Request", trimOutput: false)
let response = request.flatMapLatest { page in
RxAPIProvider.shared.getPostList(page: page).materialize()
}
.share(replay: 1)
.elements()
.debug("Response", trimOutput: false)
Observable
.combineLatest(request, response, posts.asObservable()) { request, response, posts in
return self.pageIndex == 0 ? response : posts + response
}
.sample(response)
.bind(to: posts)
.disposed(by: disposeBag)
Observable
.merge(request.map{ _ in true },
response.map { _ in false },
error.map { _ in false })
.bind(to: loading)
.disposed(by: disposeBag)
}
}
The refreshTrigger and loadNextPageTrigger is bind to difference target likes:
self.tableView.rx_reachedBottom
.map { _ in () }
.bind(to: self.viewModel.loadNextPageTrigger)
.disposed(by: disposeBag)
self.refreshControl.rx.controlEvent(.valueChanged)
.bind(to: self.viewModel.refreshTrigger)
.disposed(by: disposeBag)
self.rx.sentMessage(#selector(UIViewController.viewWillAppear(_:)))
.map { _ in () }
.bind(to: viewModel.refreshTrigger)
.disposed(by: disposeBag)
Question:
When I scroll the tableView to bottom and trigger the loadNextPageTrigger, everything works fine.
However, if there is no more data in the next coming request, the loadnextPageTrigger will be triggered infinitely.
Any help would be appreciated.
You can download the Demo here.
Main idea is to check if all elements is loaded. I found in source code that you load parts by 20, so this condition should works fine if loadedPosts < 20 then stop loading new part. Here i share with you basic solution that you can refactor as you want (because i'm not good at RxSwift):
In MomentViewModel you should declare property
private var isAllLoaded = false
that set to true if you loaded all values. Then you should check every [Post] that came in response to set correct isAllLoaded:
Observable
.combineLatest(request, response, posts.asObservable()) { request, response, posts in
self.isAllLoaded = response.count < 20 // here the check
return self.pageIndex == 0 ? response : posts + response
}
.sample(response)
.bind(to: posts)
.disposed(by: disposeBag)
And then in nextPageRequest in you should return .empty() observer if all parts have been loaded:
if loading {
return Observable.empty()
} else {
guard !self.isAllLoaded else { return Observable.empty() }
return Observable<Int>.create { [unowned self] observer in
self.pageIndex += 1
print(self.pageIndex)
observer.onNext(self.pageIndex)
observer.onCompleted()
return Disposables.create()
}
}
P.S. The MomentViewModel.swift file to copy/paste.