I'm creating an app using Firestore, I have a function that supposed to add a user to another user's friends list and return true if it was done successfully.
This is the function:
static func addFriendToList(_ id: String) -> Bool {
var friend: Friend!
var isSuccessfullyAddedFriend: Bool = false
let group = DispatchGroup()
DispatchQueue.global(qos: .userInitiated).async {
group.enter()
// Getting user's deatils and creating a Friend object.
FirestoreService.shared.getUserDetailsById(user: id) { (newFriend) in
if newFriend != nil {
friend = newFriend!
}
group.leave()
}
group.wait()
group.enter()
// Adding the new Friend Object to the friends list of the current user
FirestoreService.shared.addUserToFriendsList(friend: friend) { (friendAdded) in
if friendAdded {
isSuccessfullyAddedFriend = true
FirestoreService.shared.fetchFriendList()
}
}
group.leave()
}
group.wait()
return isSuccessfullyAddedFriend
}
My problem is that the addUserToFriendsList is an async function, and the return isSuccessfullyAddedFriend is being executed before it turns to true.
As you can see, I tried to overcome this problem with using DispatchGroup, but with no success, the problem still occurs. Is there another, maybe better way to achieve this?
I need the return line to happen after addUserToFriendsList
You need
static func addFriendToList(_ id: String,completion:#escaping(Bool)->()) {
FirestoreService.shared.getUserDetailsById(user: id) { (newFriend) in
FirestoreService.shared.addUserToFriendsList(friend: newFriend) { (friendAdded) in
if friendAdded {
FirestoreService.shared.fetchFriendList()
completion(true)
}
else {
completion(false)
}
}
}
}
Call
Api.addFriendToList(<#id#>) { flag in
print(flag)
}
2 notes
1- Firebase calls run in a background thread so no need for global queue
2- DispatchGroup is used for multiple concurrent tasks not serial
Related
I am trying to build RxSwift Auth token refresh service using following tutorial: https://www.donnywals.com/building-a-concurrency-proof-token-refresh-flow-in-combine/. However, I faced with issue, when user don't have an auth token and first refresh failed, but second refresh succeed, additional request is send, and after this (3-rd request) is completed, only then called main endpoint
So, what I see in network inspector:
request to refresh token (failed)
request to refresh token (succeed)
request to refresh token (succeed)
request to main endpoint (succeed)
But it should be:
request to refresh token (failed)
request to refresh token (succeed)
request to main endpoint (succeed)
I have following code for Authenticator
protocol AuthenticatorType {
func authenticate() -> Observable<Void>
func checkForValidAuthTokenOrRefresh(forceRefresh: Bool) -> Observable<Void>
}
extension AuthenticatorType {
func checkForValidAuthTokenOrRefresh(forceRefresh: Bool = false) -> Observable<Void> {
return checkForValidAuthTokenOrRefresh(forceRefresh: forceRefresh)
}
}
final class Authenticator<Provider: RxMoyaProviderType> where Provider.Target == AuthAPI {
private let provider: Provider
private let cookiesStorageProvider: CookiesStorageProviderType
private let queue = DispatchQueue(label: "Autenticator.\(UUID().uuidString)")
private var refreshInProgressObservable: Observable<Void>?
init(
provider: Provider,
cookiesStorageProvider: CookiesStorageProviderType
) {
self.provider = provider
self.cookiesStorageProvider = cookiesStorageProvider
}
func checkForValidAuthTokenOrRefresh(forceRefresh: Bool = false) -> Observable<Void> {
return queue.sync { [weak self] in
self?.getCurrentTokenOrRefreshIfNeeded(forceRefresh: forceRefresh) ?? .just(())
}
}
func authenticate() -> Observable<Void> {
provider.request(.authenticate(credentials: .defaultDebugAccount))
.map(LoginResponse.self)
.map { loginResponse in
guard loginResponse.login else {
throw AuthenticationError.loginRequired
}
}
.asObservable()
}
}
// MARK: - Helper methods
private extension Authenticator {
func getCurrentTokenOrRefreshIfNeeded(forceRefresh: Bool = false) -> Observable<Void> {
if let refreshInProgress = refreshInProgressObservable {
return refreshInProgress
}
if cookiesStorageProvider.isHaveValidAuthToken && !forceRefresh {
return .just(())
}
guard cookiesStorageProvider.isHaveValidRefreshToken else {
return .error(AuthenticationError.loginRequired)
}
let refreshInProgress = provider.request(.refreshToken)
.share()
.map { response in
guard response.statusCode != 401 else {
throw AuthenticationError.loginRequired
}
return response
}
.map(RefreshReponse.self)
.map { refreshResponse in
guard refreshResponse.refresh else {
throw AuthenticationError.loginRequired
}
}
.asObservable()
.do(
onNext: { [weak self] _ in self?.resetProgress() },
onError: { [weak self] _ in self?.resetProgress() }
)
refreshInProgressObservable = refreshInProgress
return refreshInProgress
}
func resetProgress() {
queue.sync { [weak self] in
self?.refreshInProgressObservable = nil
}
}
}
And thats how I refresh doing request (with logics to refresh token)
func request(_ token: Target, callbackQueue: DispatchQueue?) -> Observable<Response> {
authenticator.checkForValidAuthTokenOrRefresh()
.flatMapLatest { [weak self] res -> Observable<Response> in
self?.provider.request(token).asObservable() ?? .empty()
}
.map { response in
guard response.statusCode != 401 else {
throw AuthenticationError.loginRequired
}
return response
}
.retry { [weak self] error in
error.flatMap { error -> Observable<Void> in
guard let authError = error as? AuthenticationError, authError == .loginRequired else {
return .error(error)
}
return self?.authenticator.checkForValidAuthTokenOrRefresh(forceRefresh: true) ?? .never()
}
}
}
At first, I thought it was concurrency problem, I changed queue to NSLock, but it all was the same. Also I tried to use subscribe(on:) and observe(on:), thats also don't give any effect.
Maybe issue with do block, where I set refreshInProgressObservable to nil, because when I change onError, to afterError, I don't see third request to refresh token, but I also don't see any request to main endpoint.
I even tried to remove share(), but as you guess it don't help either.
Ah, and also I remember that 3-rd request fires instantly after second is completed, even if I add sleep in beginning of getCurrentTokenOrRefreshIfNeeded method. So that kinda strange
Edit
I tried another way to refresh token, using deferred block in Observable (inspired by Daniel tutorial).
Here is my code
final class NewProvider {
let authProvider: MoyaProvider<AuthAPI>
let apiProvider: MoyaProvider<AppAPI>
let refreshToken: Observable<Void>
init(authProvider: MoyaProvider<AuthAPI>, apiProvider: MoyaProvider<AppAPI>) {
self.authProvider = authProvider
self.apiProvider = apiProvider
refreshToken = authProvider.rx.request(.refreshToken)
.asObservable()
.share()
.map { _ in }
.catchAndReturn(())
}
func request(_ token: AppAPI) -> Observable<Response> {
Observable<Void>
.deferred {
if CookiesStorageProvider.isHaveValidAuthToken {
return .just(())
} else {
throw AuthenticationError.loginRequired
}
}
.flatMapLatest { [weak self] _ in
self?.apiProvider.rx.request(token).asObservable() ?? .never()
}
.retry { [weak self] error in
return error.flatMapLatest { [weak self] _ in
self?.refreshToken ?? .never()
}
}
}
}
It works perfectly for one request (like, "it sends request to refresh token only when auth token is missing and try to refresh token again if token refresh failed")
However, there is problem with multiple requests. If there is no auth token and multiple request are fired, it works well, requests are waiting for token to refresh. BUT, if token refresh failed, there is no attempt to try refresh token again. I don't know what can lead to this behaviour.
EDIT 2
I found out that if I place
.observe(on: SerialDispatchQueueScheduler(queue: queue, internalSerialQueueName: "test1"))
after
.share()
refreshToken = authProvider.rx.request(.refreshToken)
.asObservable()
.share()
.observe(on: SerialDispatchQueueScheduler(queue: queue, internalSerialQueueName: "test1"))
.map { _ in }
.catchAndReturn(())
All will be work as expected, but now I can't understand why its working this way
Okay, I pulled down your code and spent a good chunk of the day looking it over. A couple of review points:
This is way more complex than it needs to be for what it's doing.
Any time you have a var Observable, you are doing something wrong. Observables and Subjects should always be let.
There is no reason or need to use a DispatchQueue the way you did for Observables. This code doesn't need one at all, but even if it did, you should be passing in a Scheduler instead of using queues directly.
I could see no way for your code to actually use the new token in the retry once it has been received. Even if these tests did pass, the code still wouldn't work.
As far as this specific question is concerned. The fundamental problem is that you are calling getCurrentTokenOrRefreshIfNeeded(forceRefresh:) four times in the offending test and creating three refreshInProgress Observables. You are making three of them, because the second one has emitted a result and been disposed before the last call to the function is made. Each one emits a value so you end up with three next events in authAPIProviderMock.recordedEvents.
What is the fix? I could not find a fix without making major changes to the basic structure/architecture of the code. All I can do at this point is suggest that you check out my article on this subject RxSwift and Handling Invalid Tokens which contains working code for this use case and includes unit tests. Or revisit Donny's article which I presume works, but since there are no unit tests for his code, I can't be sure.
Edit
In answer to your question in the comments, here is how you would solve the problem using my service class:
First create a tokenAcquisitionService object. Since you don't actually need to pass a token value around, just use Void for the token type.
let service = TokenAcquisitionService(initialToken: (), getToken: { _ in URLSession.shared.rx.response(request: refreshTokenRequest) }, extractToken: { _ in })
(Use whatever you want in place of URLSession.shared.rx.response(request: refreshTokenRequest). The only requirement is that it returns an Observable<(response: HTTPURLResponse, data: Data)> and in this case the data can simply be Data() or anything else, since it is ignored. It can even present a view controller that asks the user to login.)
Now at the end of every request, include the following.
.do(onNext: { response in
guard response.response.statusCode != 401 else { throw TokenAcquisitionError.unauthorized }
})
.retry(when: { $0.renewToken(with: tokenAcquisitionService) })
Wrap the above however you want so you don't have to copy pasted it onto every request.
QED
I'm trying to inflate my layout with data, it works but on some attempts user filed or comments field just have still nil and it fails. How can I can optimise the requests? I'm still beginner with swift and I cannot wrap my had around this.
The class that fails is the findUserById which does just what it is in the name. So find the user for specific id from the users list. But sometimes users are not yet initalized and the error with force unwraping is visible
func findUserByUserId(UserId:Int) -> User{
var userPlaceholder : User!
for user in self.users {
if(user.id == UserId){
userPlaceholder = user
}
}
return userPlaceholder
}
This is the function that I'm using for setting up the structure of the table cell
private func fetchPost() {
self.posts.forEach { (post) in
DispatchQueue.global(qos: .userInteractive).async(group: dispatchGroup) {
self.dispatchGroup.enter()
self.postsCellViewModels.append(PostsCellViewModel(post: post, user: self.findUserByUserId(UserId: post.userId), comments: self.findCommensByPostId(PostId: post.id)))
self.dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
os_log("PostsViewModel -> Finished fetching posts")
self.isLoading = false
self.reloadData?()
}
And this is function for fetchingUsers from API, i have the same for posts and comments, data is taken from JSONPlaceholderAPI
func fetchUsers(){
dispatchGroup.enter()
os_log("PostsViewModel -> Starting users fetching")
if let client = client as? JSONPlaceholderClient {
self.isLoading = true
let endpoint = JsonPlaceHolderEndpoint.users
client.fetchUsers(with: endpoint) { (either) in
switch either {
case .success(let users):
self.users = users
os_log("PostsViewModel -> Ended users fetching")
self.dispatchGroup.leave()
case .error(let error):
self.showError?(error)
}
}
}
}
I do understand, that all request from firebase are async.
I have collection tasksCategory -> document -> subcollection tasks
This is my class for getting all created tasks category, there is no problem. Problem is that I need to retrieve all tasks for each category by passing document ID.
class fsTasks: ObservableObject {
#Published var categories = [fsTaskCategory]()
init() {
fsGetTaskCategories()
}
/// Retrieve Tasks Categories For Logged In User
func fsGetTaskCategories() {
db.collection("tasksCategories").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.categories = documents.compactMap { queryDocumentSnapshot -> fsTaskCategory? in
return try? queryDocumentSnapshot.data(as: fsTaskCategory.self)
}
}
}
}
I have create another function to retrieve all tasks for each passed document ID
func fsGetTasks(documentID: String, completation: #escaping([fsTask]) -> Void) {
var tasks = [fsTask]()
db.collection("tasksCategories").document(documentID).collection("tasks").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
tasks = documents.compactMap { queryDocumentSnapshot -> fsTask? in
return try? queryDocumentSnapshot.data(as: fsTask.self)
}
completation(tasks)
}
}
Problem is that I do not have any idea, how can I call this function directly in the view of SWIFTUI.
Basically I have first ForEach through the ObservedObject of all categories, then I need to do another foreach for all tasks for each category, but first I need to retrieve data. I need function which return an array with all tasks retrieved from firebase but only when completation handler return data.
If I have function like this
func retrieveAllTasks(categoryID: String) -> [fsTasks] {
var fetchedTasks = [fsTasks]()
fsGetTasks(documentID: categoryID, completation: { (tasks) in
fetcheTasks = tasks
})
return fetchedTasks
}
I was still retrieving only empty array.
This is the issue
func retrieveAllTasks(categoryID: String) -> [fsTasks] {
var fetchedTasks = [fsTasks]()
fsGetTasks(documentID: categoryID, completation: { (tasks) in
fetcheTasks = tasks
})
return fetchedTasks
}
This is an asynchronous function as well (see the closure) and you have to give Firebase time to retrieve the data from the server and handle it within the Firebase closure.
What's happening here is that while you are doing that within the Firebase closure itself, that's not happening within this closure. return fetchedTasks is returning before fetchedTasks = tasks.
I would call the firebase function directly since it doesn't appear you need the middleman retrieveAllTasks function
self.fsGetTasks(documentID: "some_doc", completion: { taskArray in
for task in taskArray {
print(task
}
})
If you do, you need to add an #escaping clause to that as well and not use return fetchedTasks
I am trying to get the element of 2 observables produced asynchronously and pass them as parameters to a function once both are received.
However my map operator in my ViewModel below is not executed and the breakpoint just skips over it.
ViewModel.swift
init(api: ApiService) {
self.api = api
}
func getData1() -> Observable<Data1> {
return api.getData1()
}
func getData2() -> Observable<NewViewModel> {
return Observable.create { observer in
let disposable = Disposables.create()
self.api.getData2()
.map {
$0.arrayOfStuff.forEach { (stuff) in
let background = stuff.background
let newViewModel = NewViewModel( background: self.spotlightBackground)
observor.onNext(newViewModel)
}
return disposable
}
}
In my ViewController i am creating the Zip of the observables because newViewModel[getData2] may return later and i want to call the function when both observables emit a value
in my viewDidLoad() i setup zip by subscribing and adding observables
let zippy = Observable.zip(viewModel.getData1(), viewModel.getData2()).subscribe(onNext: { (data1, newViewModel) in
self.layoutSetUp(data1: data1, newViewModel: newViewModel)
})
zippy.disposed(by: disposeBag)
private func layoutSetUp(data1: Data1, newViewModel: NewViewModel) {
DispatchQueue.main.async {
self.view = SwiftUIHostingView(rootView: SwiftUIContentView(data1: data1, newViewModel: newViewModel))
}
}
This is not executing and no values are passed to function either and im not sure why
Your getData2 method never emits a value so neither will the zip. The code in the method is a bit too muddled for me to understand what you are trying to do so I can't tell you exactly what you need, but I can say that when you have an observable that nothing is subscribed to, then it will not emit a value.
This bit:
self.api.getData2()
.map {
$0.arrayOfStuff.forEach { (stuff) in
let background = stuff.background
let newViewModel = NewViewModel(background: self.spotlightBackground)
observor.onNext(newViewModel)
}
return disposable
}
Is an observable with no subscribers.
For communication with backend during the checkout process I have the async functions:
create() : Creates the cart on backend. Called when user segues to the checkout page.
update() : Edits the cart on backend. Called when user edits the cart.
confirm() : Confirms purchase on backend. Called when user places the order.
update() is dependent on response from create(), confirm() is dependent on response from create()/update()
The user can call one function while another is unfinished e.g edits cart shortly after segue to checkout page. This causes problems due to the dependencies.
I have currently semi-solved it by using the bools processing, shouldUpdate and shouldConfirm.
Is there a way to achieve by using a queue where the next function call waits until the previous has finished?
var processing = false // Set true when a function is executing
var shouldUpdate = false // Set true when user edits cart
var shouldConfirm = false // Set true when user taps "Purchase"
var checkoutID = ""
func create() {
processing = true
APIClient.sharedClient.createShoppingCart() {
(checkoutID, error) in
...
processing = false // Finished with network call
if shouldUpdate { // if edit was done while create() is running
update()
shouldUpdate = false
}
if shouldConfirm { // if user tapped "Purchase" while create() is running
confirm()
}
}
}
func update() { // Called from view controller or create()
if processing {return}
processing = true
APIClient.sharedClient.updateShoppingCart(forCheckoutID: checkoutID) {
(error) in
...
processing = false // Finished with network call
if shouldConfirm { // if user tapped "Purchase" while update() is running
confirm()
}
}
}
func confirm() { // Called from view controller or create()/update()
if processing {return}
APIClient.sharedClient.confirmPurchase(forCheckoutID: checkoutID) {
(error) in
...
/// Finish order process
}
}
I personally use PromiseKit - Nice article generally here, wrapping async here - and how to promises here
// your stack
var promises = [];
// add to your stack
promises.push(promise); // some promise func, see above links
promises.push(promise2);
// work the stack
when(fulfilled: promiseArray).then { results in
// Do something
}.catch { error in
// Handle error
}
Keywords for similar solutions: Promises, Deferred, Async Stacks.
or:
You could implement the following:
Have a pool, array of tupel: methodhandler and bool (=executed true)
create a func(1) runs all funcs from the array, in another wrapper function(2) that will set the tupels bool when it is executed.
func(1) will wait till the tupel is changed, and grab the next one.
You can use Dispatch Group
let apiDispatchGroup = DispatchGroup()
func asyncCall1() {
apiDispatchGroup.enter()
print("Entered")
DispatchQueue.main.asyncAfter(deadline: .now()+3) {
/// After 3 Second it will notify main Queue
print("Task 1 Performmed")
/// Let's notify
apiDispatchGroup.leave()
}
apiDispatchGroup.notify(queue: .main) {
/// Perform task 2
asyncCall2()
}
}
func asyncCall2() {
print("Task 2")
}