Swift async/await & PromiseKit - swift

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 !

Related

How to run two snapshot listeners functions same time on start and after it separately

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

#Published is not getting updated, State problem? - SwiftUI

Right now I have to call the function (calculatePortfolioGrossBalance) 3 times for the value to update, what am I doing wrong in the state logic?
In the code below, when I call in an init the function calculatePortfolioGrossBalance() it returns empty [], I have to call it 3 times for the value to update, However... if I print the values of getTokenBalancesModel in the line DispatchQueue.main.async { I can see the values are there, so how come in calculatePortfolioGrossBalance are not?
final class TokenBalancesClassAViewModel: ObservableObject {
#Published var getTokenBalancesModel: [TokenBalancesItemsModel] = [TokenBalancesItemsModel]()
#Published var portfolioGrossBalance: String = "0.0"
func calculatePortfolioGrossBalance() {
getTokenBalances()
DispatchQueue.main.async {
var totalBalance: Double = 0
for item in self.getTokenBalancesModel {
totalBalance += Double(item.quote!)
}
self.portfolioGrossBalance = String(format:"%.2f", totalBalance)
print(self.portfolioGrossBalance)
}
}
func getTokenBalances() {
guard let url = URL(string: "someUrlHeidiGaveMe") else {
print("Invalid URL")
return
}
print("Calling getTokenBalances() ...")
AF.request(url, method: .get).validate().responseData(completionHandler: { data in
do {
guard let data = data.data else {
print("Response Error:", data.error as Any)
return
}
let apiJsonData = try JSONDecoder().decode(TokenBalancesModel.self, from: data)
DispatchQueue.main.async {
self.getTokenBalancesModel = apiJsonData.data.items
}
} catch {
print("ERROR:", error)
}
})
}
}
you need to read up on using asynchronous functions, how to set them up and how to use them. This is important. Try something like this (untested):
final class TokenBalancesClassAViewModel: ObservableObject {
#Published var getTokenBalancesModel: [TokenBalancesItemsModel] = [TokenBalancesItemsModel]()
#Published var portfolioGrossBalance: String = "0.0"
func calculatePortfolioGrossBalance() {
getTokenBalances() { isGood in
if isGood {
var totalBalance: Double = 0
for item in self.getTokenBalancesModel {
totalBalance += Double(item.quote!)
}
self.portfolioGrossBalance = String(format:"%.2f", totalBalance)
print(self.portfolioGrossBalance)
}
}
}
func getTokenBalances(completion: #escaping (Bool) -> Void) {
guard let url = URL(string: "someUrlHeidiGaveMe") else {
print("Invalid URL")
completion(false)
return
}
print("Calling getTokenBalances() ...")
AF.request(url, method: .get).validate().responseData(completionHandler: { data in
do {
guard let data = data.data else {
print("Response Error:", data.error as Any)
completion(false)
return
}
let apiJsonData = try JSONDecoder().decode(TokenBalancesModel.self, from: data)
DispatchQueue.main.async {
self.getTokenBalancesModel = apiJsonData.data.items
completion(true)
}
} catch {
print("ERROR:", error)
completion(false)
}
})
}
}

How to write unit tests for Firestore

I am a beginner and I need to test my FirestoreService class below. I tried to make a mock but I do not go in the snapshot. I just need to test the case where it fails.
class FirebaseService {
let decoder = JSONDecoder()
var movies = [Movies]()
enum Result {
case success([Movies])
case failure(Error)
}
func getMovies(completion: #escaping (Result) -> Void) {
let movieReference = Firestore.firestore().collection("movies").order(by: "id")
movieReference.addSnapshotListener { (snapshot, _) in
guard let snapshot = snapshot else {return}
do {
self.movies = try snapshot.decoded()
completion(.success(self.movies))
} catch {
completion(.failure(error))
}
}
}
This is my FirebaseServiceTests class
XCTestclass FirebaseServiceTests: XCTestCase {
func testGetMovies() {
let firebaseSercice = FirebaseService()
let expectation = XCTestExpectation(description: "Wait for queue change ")
firebaseSercice.getMovies { result in
XCTAssertNotNil(result)
expectation.fulfill()
}
wait(for: [expectation], timeout: 0.5)
}
func testGetMoviesWithMock() {
let firebaseSercice = MockDatabaseReference()
let expectation = XCTestExpectation(description: "Wait for queue change ")
firebaseSercice.getMovies { result in
XCTAssertNil(result)
expectation.fulfill()
}
wait(for: [expectation], timeout: 0.5)
}
}
private class MockDatabaseReference: FirebaseService {
override func getMovies(completion: #escaping (Result) -> Void) {
let movieTestReference = Firestore.firestore().collection("mov").order(by: "id")
movieTestReference.addSnapshotListener { (snapshot, _) in
guard let snapshot = snapshot else {return}
do {
self.movies = try snapshot.decoded()
completion(.success(self.movies))
} catch {
completion(.failure(error))
}
}
}
}
Could you explain me how to do ? Thank you.

How to notify a queue in Swift (GCD)

I'm using GCD to notify main thread (have 2 async calls inside the function)
My code:
func getWavesByMostRecent(closure: #escaping ([Wave]?) -> Void) {
var waves = [Wave]()
let dispatchGroup = DispatchGroup()
self.query = DatabaseManager.waveRef.queryOrdered(byChild: Constants.reverseTimeStampKey)
self.handle = self.query?.observe(.value, with: { (snapshot) in
for value in snapshot.children {
guard let wave = Wave(snapshot: value as! DataSnapshot) else { return }
self.geoFire = GeoFire(firebaseRef: DatabaseManager.waveRef)
let currentLocation = LocationManager.shared.getCurrentLocation()
dispatchGroup.enter()
self.geoFire?.getLocationForKey(wave.waveID, withCallback: { (location, error) in
guard let location = location else { return }
if error == nil {
if location.distance(from: currentLocation) < Constants.distance {
print("Wave", wave.waveID, "is in range")
waves.append(wave)
} else {
print("Wave", wave.waveID, "is out of range")
}
} else {
print(error?.localizedDescription ?? "")
}
dispatchGroup.leave()
})
}
dispatchGroup.notify(queue: .main) {
print("THERE ARE SO MANY WAVES:", waves.count)
closure(waves)
}
})
}
But .notify closure just doesn't work and I cannot call my "main" closure right. What am I doing wrong? Any advice would be appreciated.
Try this change:
self.geoFire?.getLocationForKey(wave.waveID, withCallback: { (location, error) in
defer { dispatchGroup.leave() }
guard let location = location else { return }
if error == nil {
if location.distance(from: currentLocation) < Constants.distance {
print("Wave", wave.waveID, "is in range")
waves.append(wave)
} else {
print("Wave", wave.waveID, "is out of range")
}
} else {
print(error?.localizedDescription ?? "")
}
})
As noted in matt's comment defer is a good tool to do something always when leaving.
This is another issue, but updating an Array from multiple thread simultaneously would cause some problems. It rarely happens, so it can be a hard-to-fix bug.
I'm not sure if GeoFire calls its callback in the main thread or not, but if not, you should better enclose all the callback code in DispatchQueue.main.async {...}.
dispatchGroup.leave() is still in the closure, instead it should be at the end of the for loop like this:
func getWavesByMostRecent(closure: #escaping ([Wave]?) -> Void) {
var waves = [Wave]()
let dispatchGroup = DispatchGroup()
self.query = DatabaseManager.waveRef.queryOrdered(byChild: Constants.reverseTimeStampKey)
self.handle = self.query?.observe(.value, with: { (snapshot) in
for value in snapshot.children {
guard let wave = Wave(snapshot: value as! DataSnapshot) else { return }
self.geoFire = GeoFire(firebaseRef: DatabaseManager.waveRef)
let currentLocation = LocationManager.shared.getCurrentLocation()
dispatchGroup.enter()
self.geoFire?.getLocationForKey(wave.waveID, withCallback: { (location, error) in
guard let location = location else { return }
if error == nil {
if location.distance(from: currentLocation) < Constants.distance {
print("Wave", wave.waveID, "is in range")
waves.append(wave)
} else {
print("Wave", wave.waveID, "is out of range")
}
} else {
print(error?.localizedDescription ?? "")
}
})
dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main) {
print("THERE ARE SO MANY WAVES:", waves.count)
closure(waves)
}
})
}

RxSwift pagination

I can't manage to get this solution to work:
https://github.com/liuznsn/RxMoyaPaginationNetworking
Maybe someone can tell me where is the mistake. The loading variable never goes to false. I guess the issue is in the request observable, but I can't find out why.
class PaginationNetworkModel<T1: Mappable>: NSObject {
let refreshTrigger = PublishSubject<Void>()
let loadNextPageTrigger = PublishSubject<Void>()
let loading = Variable<Bool>(false)
let elements = Variable<[T1]>([])
var offset:Int = 0
let error = PublishSubject<Swift.Error>()
private let disposeBag = DisposeBag()
override init() {
super.init()
let refreshRequest = loading.asObservable()
.sample(refreshTrigger)
.flatMap { [unowned self] loading -> Observable<[T1]> in
if loading {
return Observable.empty()
} else {
return self.loadData(offset: self.offset)
}
}
let nextPageRequest = loading.asObservable()
.sample(loadNextPageTrigger)
.flatMap { [unowned self] loading -> Observable<[T1]> in
if loading {
return Observable.empty()
} else {
self.offset += 1
return self.loadData(offset: self.offset)
}
}
let request = Observable
.of(refreshRequest, nextPageRequest)
.merge()
.shareReplay(1)
let response = request.flatMap { events -> Observable<[T1]> in
request
.do(onError: { error in
self.error.onNext(error)
}).catchError({ error -> Observable<[T1]> in
Observable.empty()
})
}.shareReplay(1)
Observable
.combineLatest(request, response, elements.asObservable()) { [unowned self] request, response, elements in
return self.offset == 0 ? response : elements + response
}
.sample(response)
.bind(to: elements)
.addDisposableTo(rx_disposeBag)
Observable
.of(request.map { _ in true },
response.map { $0.count == 0 },
error.map { _ in false }
)
.merge()
.bind(to: loading)
.addDisposableTo(rx_disposeBag)
}
func loadData(offset: Int) -> Observable<[T1]> {
return Observable.empty()
}
Thank you Daniel for help, here completed solution
Call example:
goodsModel = GoodsNetworkModel()
goodsModel.elements.asObservable().bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: StoreCell.self)) { (ip, item: Goods, cell: StoreCell) in
cell.configure(goods: item)
}.addDisposableTo(rx_disposeBag)
tableView.rx.itemSelected.bind() { [unowned self] ip in
self.didSelectRow(ip: ip.row)
}.addDisposableTo(rx_disposeBag)
rx.sentMessage(#selector(UIViewController.viewDidAppear(_:)))
.map { _ in () }
.bind(to: goodsModel.refreshTrigger)
.addDisposableTo(rx_disposeBag)
tableView.rx_reachedBottom
.map{ _ in ()}
.bind(to: goodsModel.loadNextPageTrigger)
.addDisposableTo(rx_disposeBag)
Model code:
class PaginationNetworkModel<T1: Mappable>: NSObject {
let refreshTrigger = PublishSubject<Void>()
let loadNextPageTrigger = PublishSubject<Void>()
let loading = Variable<Bool>(false)
let elements = Variable<[T1]>([])
var offset:Int = 0
let error = PublishSubject<Swift.Error>()
private let disposeBag = DisposeBag()
override init() {
super.init()
let refreshRequest = loading.asObservable()
.sample(refreshTrigger)
.flatMap { loading -> Observable<Int> in
if loading {
return Observable.empty()
} else {
return Observable<Int>.create { observer in
observer.onNext(0)
observer.onCompleted()
return Disposables.create()
}
}
}
let nextPageRequest = loading.asObservable()
.sample(loadNextPageTrigger)
.flatMap { [unowned self] loading -> Observable<Int> in
if loading {
return Observable.empty()
} else {
return Observable<Int>.create { [unowned self] observer in
self.offset += 1
observer.onNext(self.offset)
observer.onCompleted()
return Disposables.create()
}
}
}
let request = Observable
.of(refreshRequest, nextPageRequest)
.merge()
.shareReplay(1)
let response = request.flatMap { offset -> Observable<[T1]> in
self.loadData(offset: offset)
.do(onError: { [weak self] error in
self?.error.onNext(error)
}).catchError({ error -> Observable<[T1]> in
Observable.empty()
})
}.shareReplay(1)
Observable
.combineLatest(request, response, elements.asObservable()) { [unowned self] request, response, elements in
return self.offset == 0 ? response : elements + response
}
.sample(response)
.bind(to: elements)
.addDisposableTo(rx_disposeBag)
Observable
.of(request.map{_ in true},
response.map { $0.count == 0 },
error.map { _ in false })
.merge()
.bind(to: loading)
.addDisposableTo(rx_disposeBag)
}
func loadData(offset: Int) -> Observable<[T1]> {
return Observable.empty()
}
The problem is here:
let refreshRequest: Observable<[T1]> = loading.asObservable()
.sample(refreshTrigger)
.flatMap { [unowned self] loading -> Observable<[T1]> in
if loading {
return Observable.empty()
} else {
return self.loadData(offset: self.offset)
}
}
refreshRequest doesn't emit a value until after loadData returns. The way your code is structured, emitting a signal on the refreshTrigger will start the network request, and then set loading to true after the network request completes.
It would be better to have refreshRequest and nextPageRequest return an Observable of what page to load and then merge them and call flatMap with the network call on the merged result.