Chaining multiple async functions in Swift - ios

I'm trying to write a series of functions that will validate the user's information before asking them to confirm something. (Imagine a shopping app).
I first have to check that the user has added a card.
Then I have to check that they have sufficient balance.
Then I can ask them to confirm the payment.
I can write the async method to check the card something like ...
func checkHasCard(completion: (Bool) -> ()) {
// go to the inter webs
// get the card
// process data
let hasCard: Bool = // the user has a card or not.
completion(hasCard)
}
This can be run like this...
checkHasCard {
hasCard in
if hasCard {
print("YAY!")
} else {
print("BOO!")
}
}
But... now, based off that I have to do various things. If the user does have a card I then need to continue onwards and check there is sufficient balance (in much the same way). If the user does not have a card I present a screen for them to add their card.
But it gets messy...
checkHasCard {
hasCard in
if hasCard {
// check balance
print("YAY!")
checkBalance {
hasBalance in
if hasBalance {
// WHAT IS GOING ON?!
print("")
} else {
// ask to top up the account
print("BOO!")
}
}
} else {
// ask for card details
print("BOO!")
}
}
What I'd like instead is something along the lines of this...
checkHasCard() // if no card then show card details screen
.checkBalance() // only run if there is a card ... if no balance ask for top up
.confirmPayment()
This looks much more "swifty" but I'm not sure how to get closer to something like this.
Is there a way?

Asynchronous operations, ordered and with dependencies? You're describing NSOperation.
Certainly you can chain tasks using GCD:
DispatchQueue.main.async {
// do something
// check something...
// and then:
DispatchQueue.main.async {
// receive info from higher closure
// and so on
}
}
But if your operations are complex, e.g. they have delegates, that architecture completely breaks down. NSOperation allows complex operations to be encapsulated in just the way you're after.

Related

How to run a Firestore query inside a map function in Swift

I am new to SwiftUI and Firebase and I am trying to build my first app. I am storing Game documents in Firestore and one of the fields is an array containing the user ids of the players as you can see in the image.
Game data structure
That being said, I am trying to list all games of a given user and have all the players listed in each one of the cells (the order is important).
In order to create the list of games in the UI I created a GameCellListView and a GameCellViewModel. The GameCellViewModel should load both the games and the array of users that correspond to the players of each game. However I am not being able to load the users to an array. I have to go through the players array and query the database for each Id and append to a User array; then I should be able to return this User array. Since I'm using a for loop, I can't assign the values to the array and then return it. I tried using map(), but I can't perform a query inside of it.
The goal is to load that "all" var with a struct that receives a game and its players GamePlayers(players: [User], game: Game)
It should look something like the code snippet below, but the users array always comes empty. This function runs on GameCellViewModel init.
I hope you can understand my problem and thank you in advance! Been stuck on this for 2 weeks now
func loadData() {
let userId = Auth.auth().currentUser?.uid
db.collection("games")
.order(by: "createdTime")
.whereField("userId", isEqualTo: userId)
.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.games = querySnapshot.documents.compactMap { document in
do {
let extractedGame = try document.data(as: Game.self)
var user = [User]()
let users = extractedGame!.players.map { playerId -> [User] in
self.db.collection("users")
.whereField("uid", isEqualTo: playerId)
.addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
user = documents.compactMap { queryDocumentSnapshot -> User? in
return try? queryDocumentSnapshot.data(as: User.self)
}
}
return user
}
self.all.append(GamePlayers(players: users.first ?? [User](), game: extractedGame!))
return extractedGame
}
catch {
print(error)
}
return nil
}
}
}
}
There are a lot of moving parts in your code and so to isolate points of failure would require seeing additional code so just be aware of that upfront. That said, if you are relatively new to Firestore or Swift then I would strongly suggest you first get a handle on this function using basic syntax. Once you're comfortable with the ins and outs of async looping then I would suggest refactoring the code using more advanced syntax, like you have here.
Your function requires performing async work within each loop iteration (of each document). You actually need to do this twice, async work within a loop within a loop. Be sure this is what you really want to do before proceeding because there may be cleaner ways, which may include a more efficient NoSQL data architecture. Regardless, for the purposes of this function, start with the most basic syntax there is for the job which is the Dispatch Group in concert with the for-loop. Go ahead and nest these until you have it working and then consider refactoring.
func loadData() {
// Always safely unwrap the user ID and never assume it is there.
guard let userId = Auth.auth().currentUser?.uid else {
return
}
// Query the database.
db.collection("games").whereField("userId", isEqualTo: userId).order(by: "createdTime").addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
// We need to loop through a number of documents and perform
// async tasks within them so instantiate a Dispatch Group
// outside of the loop.
let dispatch = DispatchGroup()
for doc in querySnapshot.documents {
// Everytime you enter the loop, enter the dispatch.
dispatch.enter()
do {
// Do something with this document.
// You want to perform an additional async task in here,
// so fire up another dispatch and repeat these steps.
// Consider partitioning these tasks into separate functions
// for readability.
// At some point in this do block, we must leave the dispatch.
dispatch.leave()
} catch {
print(error)
// Everytime you leave this iteration, no matter the reason,
// even on error, you must leave the dispatch.
dispatch.leave()
// If there is an error in this iteration, do not return.
// Return will return out of the method itself (loadData).
// Instead, continue, which will continue the loop.
continue
}
}
dispatch.notify(queue: .main) {
// This is the completion handler of the dispatch.
// Your first round of data is ready, now proceed.
}
} else if let error = error {
// Always log errors to console!!!
// This should be automatic by now without even having to think about it.
print(error)
}
}
}
I also noticed that within the second set of async tasks within the second loop, you're adding snapshot listeners. Are you really sure you want to do this? Don't you just need a plain document get?

Make multiple asynchronous requests but wait for only one

I have a question concerning asynchronous requests. I want to request data from different sources on the web. Each source might have the data I want but I do not know that beforehand. Because I only want that information once, I don't care about the other sources as soon as one source has given me the data I need. How would I go about doing that?
I thought about doing it with a didSet and only setting it once, something like this:
var dogPicture : DogPicture? = nil {
didSet {
// Do something with the picture
}
}
func findPictureOfDog(_ sources) -> DogPicture? {
for source in sources {
let task = URL.Session.shared.dataTask(with: source) { (data, response, error) in
// error handling ...
if data.isWhatIWanted() && dogPicture == nil {
dogPicture = data.getPicture()
}
}
task.resume()
}
}
sources = ["yahoo.com", "google.com", "pinterest.com"]
findPictureOfDog(sources)
However it would be very helpful, if I could just wait until findPictureOfDog() is finished, because depending on if I find something or not, I have to ask the user for more input.
I don't know how I could do it in the above way, because if I don't find anything the didSet will never be called, but I should ask the user for a picture then.
A plus: isWhatIWanted() is rather expensive, so If there was a way to abort the execution of the handler once I found a DogPicture would be great.
I hope I made myself clear and hope someone can help me out with this!
Best regards and thank you for your time
A couple of things:
First, we’re dealing with asynchronous processes, so you shouldn’t return the DogPicture, but rather use completion handler pattern. E.g. rather than:
func findPictureOfDog(_ sources: [String]) -> DogPicture? {
...
return dogPicture
}
You instead would probably do something like:
func findPictureOfDog(_ sources: [String], completion: #escaping (Result<DogPicture, Error>) -> Void) {
...
completion(.success(dogPicture))
}
And you’d call it like:
findPictureOfDog(sources: [String]) { result in
switch result {
case .success(let dogPicture): ...
case .failure(let error): ...
}
}
// but don’t try to access the DogPicture or Error here
While the above was addressing the “you can’t just return value from asynchronous process”, the related observations is that you don’t want to rely on a property as the trigger to signal when the process is done. All of the “when first process finishes” logic should be in the findPictureOfDog routine, and call the completion handler when it’s done.
I would advise against using properties and their observers for this process, because it begs questions about how one synchronizes access to ensure thread-safety, etc. Completion handlers are unambiguous and avoid these secondary issues.
You mention that isWhatIWanted is computationally expensive. That has two implications:
If it is computationally expensive, then you likely don’t want to call that synchronously inside the dataTask(with:completionHandler:) completion handler, because that is a serial queue. Whenever dealing with serial queues (whether main queue, network session serial queue, or any custom serial queue), you often want to get in and out as quickly as possible (so the queue is free to continue processing other tasks).
E.g. Let’s imagine that the Google request came in first, but, unbeknownst to you at this point, it doesn’t contain what you wanted, and the isWhatIWanted is now slowly checking the result. And let’s imagine that in this intervening time, the Yahoo request that came in. If you call isWhatIWanted synchronously, the result of the Yahoo request won’t be able to start checking its result until the Google request has failed because you’re doing synchronous calls on this serial queue.
I would suggest that you probably want to start checking results as they came in, not waiting for the others. To do this, you want a rendition of isWhatIWanted the runs asynchronously with respect to the network serial queue.
Is the isWhatIWanted a cancelable process? Ideally it would be, so if the Yahoo image succeeded, it could cancel the now-unnecessary Pinterest isWhatIWanted. Canceling the network requests is easy enough, but more than likely, what we really want to cancel is this expensive isWhatIWanted process. But we can’t comment on that without seeing what you’re doing there.
But, let’s imagine that you’re performing the object classification via VNCoreMLRequest objects. You might therefore cancel any pending requests as soon as you find your first match.
In your example, you list three sources. How many sources might there be? When dealing with problems like this, you often want to constrain the degree of concurrency. E.g. let’s say that in the production environment, you’d be querying a hundred different sources, you’d probably want to ensure that no more than, say, a half dozen running at any given time, because of the memory and CPU constraints.
All of this having been said, all of these considerations (asynchronous, cancelable, constrained concurrency) seem to be begging for an Operation based solution.
So, in answer to your main question, the idea would be to write a routine that iterates through the sources, and calling the main completion handler upon the first success and make sure you prevent any subsequent/concurrent requests from calling the completion handler, too:
You could save a local reference to the completion handler.
As soon as you successfully find a suitable image, you can:
call that saved completion handler;
nil your saved reference (so in case you have other requests that have completed at roughly the same time, that they can’t call the completion handler again, eliminating any race conditions); and
cancel any pending operations so that any requests that have not finished will stop (or have not even started yet, prevent them from starting at all).
Note, you’ll want to synchronize the the above logic, so you don’t have any races in this process of calling and resetting the completion handler.
Make sure to have a completion handler that you call after all the requests are done processing, in case you didn’t end up finding any dogs at all.
Thus, that might look like:
func findPictureOfDog(_ sources: [String], completion: #escaping DogPictureCompletion) {
var firstCompletion: DogPictureCompletion? = completion
let synchronizationQueue: DispatchQueue = .main // note, we could have used any *serial* queue for this, but main queue is convenient
let completionOperation = BlockOperation {
synchronizationQueue.async {
// if firstCompletion not nil by the time we get here, that means none of them matched
firstCompletion?(.failure(DogPictureError.noneFound))
}
print("done")
}
for source in sources {
let url = URL(string: source)!
let operation = DogPictureOperation(url: url) { result in
if case .success(_) = result {
synchronizationQueue.async {
firstCompletion?(result)
firstCompletion = nil
Queues.shared.cancelAllOperations()
}
}
}
completionOperation.addDependency(operation)
Queues.shared.processingQueue.addOperation(operation)
}
OperationQueue.main.addOperation(completionOperation)
}
So what might that DogPictureOperation might look like? I might create an asynchronous custom Operation subclass (I just subclass a general purpose AsynchronousOperation subclass, like the one here) that will initiate network request and then run an inference on the resulting image upon completion. And if canceled, it would cancel the network request and/or any pending inferences (pursuant to point 3, above).
If you care about only one task use a completion handler, call completion(nil) if no picture was found.
var dogPicture : DogPicture?
func findPictureOfDog(_ sources, completion: #escaping (DogPicture?) -> Void) {
for source in sources {
let task = URL.Session.shared.dataTask(with: source) { (data, response, error) in
// error handling ...
if data.isWhatIWanted() && dogPicture == nil {
let picture = data.getPicture()
completion(picture)
}
}
task.resume()
}
}
sources = ["yahoo.com", "google.com", "pinterest.com"]
findPictureOfDog(sources) { [weak self] picture in
if let picture = picture {
self?.dogPicture = picture
print("picture set")
} else {
print("No picture found")
}
}
You can use DispatchGroup to run a check when all of your requests have returned:
func findPictureOfDog(_ sources: [String]) -> DogPicture? {
let group = DispatchGroup()
for source in sources {
group.enter()
let task = URLSession.shared.dataTask(with: source) { (data, response, error) in
// error handling ...
if data.isWhatIWanted() && dogPicture == nil {
dogPicture = data.getPicture()
}
group.leave()
}
task.resume()
}
group.notify(DispatchQueue.main) {
if dogPicture == nil {
// all requests came back but none had a result.
}
}
}

How to set up GCController valueChangeHandler Properly in Xcode?

I have successfully connected a steel series Nimbus dual analog controller to use for testing in both my iOS and tvOS apps. But I am unsure about how to properly set up the valueChangeHandler portion of my GCController property.
I understand so far that there are microGamepad, gamepad and extendedGamepad classes of controllers and the differences between them. I also understand that you can check to see if the respective controller class is available on the controller connected to your device.
But now I am having trouble setting up valueChangeHandler because if I set the three valueChangeHandler portions like so, then only the valueChangeHandler that works is the last one that was loaded in this sequence:
self.gameController = GCController.controllers()[0]
self.gameController.extendedGamepad?.valueChangedHandler = { (gamepad, element) -> Void in
if element == self.gameController.extendedGamepad?.leftThumbstick {
//Never gets called
}
}
self.gameController.gamepad?.valueChangedHandler = { (gamepad, element) -> Void in
if element == self.gameController.gamepad?.dpad {
//Never gets called
}
}
self.gameController.microGamepad?.valueChangedHandler = { (gamepad, element) -> Void in
if element == self.gameController.microGamepad?.dpad {
//Gets called
}
}
If I switch them around and call self.gameController.extendedGamepad.valueChangeHandler... last, then those methods will work and the gamepad and microGamepad methods will not.
Anyone know how to fix this?
You test which profile is available and depending on the profile, you set the valueChangedHandler.
It's important to realise that the extendedGamepad contains most functionality and the microGamepad least (I think the microGamepad is only used for the AppleTV remote). Therefore the checks should be ordered differently. An extendedGamepad has all functionality of the microGamepad + additional controls so in your code the method would always enter the microGamepad profile.
Apple uses the following code in the DemoBots example project:
private func registerMovementEvents() {
/// An analog movement handler for D-pads and movement thumbsticks.
let movementHandler: GCControllerDirectionPadValueChangedHandler = { [unowned self] _, xValue, yValue in
// Code to handle movement here ...
}
#if os(tvOS)
// `GCMicroGamepad` D-pad handler.
if let microGamepad = gameController.microGamepad {
// Allow the gamepad to handle transposing D-pad values when rotating the controller.
microGamepad.allowsRotation = true
microGamepad.dpad.valueChangedHandler = movementHandler
}
#endif
// `GCGamepad` D-pad handler.
// Will never enter here in case of AppleTV remote as the AppleTV remote is a microGamepad
if let gamepad = gameController.gamepad {
gamepad.dpad.valueChangedHandler = movementHandler
}
// `GCExtendedGamepad` left thumbstick.
if let extendedGamepad = gameController.extendedGamepad {
extendedGamepad.leftThumbstick.valueChangedHandler = movementHandler
}
}

Setting a subview.hidden = false locks up UI for many seconds

I'm using a button to populate a UIPickerView on a hidden UIVisualEffectView. The user clicks the button, the VisualEffectView blurs everything else, and the PickerView displays all the names in their contact list (I'm using SwiftAddressBook to do this.)
This works fine except when the user clicks the button, the UI locks up for about 5-10 seconds. I can't find any evidence of heavy CPU or memory usage. If I just print the sorted array to the console, it happens almost immediately. So something about showing the window is causing this bug.
#IBAction func getBffContacts(sender: AnyObject) {
swiftAddressBook?.requestAccessWithCompletion({ (success, error) -> Void in
if success {
if let people = swiftAddressBook?.allPeople {
self.pickerDataSource = [String]()
for person in people {
if (person.firstName != nil && person.lastName != nil) {
//println("\(person.firstName!) \(person.lastName!)")
self.pickerDataSource.append(person.firstName!)
}
}
//println(self.pickerDataSource)
println("done")
self.sortedNames = self.pickerDataSource.sorted { $0.localizedCaseInsensitiveCompare($1) == NSComparisonResult.OrderedAscending }
self.pickerView.reloadAllComponents()
self.blurView.hidden = false
}
}
else {
//no success, access denied. Optionally evaluate error
}
})
}
You have a threading issue. Read. The. Docs!
requestAccessWithCompletion is merely a wrapper for ABAddressBookRequestAccessWithCompletion. And what do we find there?
The completion handler is called on an arbitrary queue
So your code is running in the background. And you must never, never, never attempt to interact with the user interface on a background thread. All of your code is wrong. You need to step out to the main thread immediately at the start of the completion handler. If you don't, disaster awaits.

How to wait until Current Location dialog is answered to run a method?

What is the best practice for pushing a view controller based on the feedback from the requesting Current Location iOS-based dialog (shown in the image below)?
I am trying to determine after the selections is made to either success -> send the user onward in the flow OR fail -> show a screen that requires they allow current location.
I've gotten as far as calling this method from a method where a button-press allocates CLLocationManager:
- (void) confirmInfo {
BOOL locationAllowed = [CLLocationManager locationServicesEnabled];
if (locationAllowed==NO) {
// Show the how-to viewcontroller
} else {
// Go to the next step onboarding
}
}
But I do not know how to wait until the input comes back from the dialog to choose which VC to show the user.
You can have Delegate method user will allow or even if user dont allow.. and from that response you can continue you push pop.
Else even is you can try block based structure to maintain your flow.
following link may help you..
https://github.com/intuit/LocationManager
or
https://github.com/FuerteInternational/FTLocationManager
INTULocationManager *locMgr = [INTULocationManager sharedInstance];
[locMgr requestLocationWithDesiredAccuracy:INTULocationAccuracyCity
timeout:10.0
delayUntilAuthorized:YES // This parameter is optional, defaults to NO if omitted
block:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) {
if (status == INTULocationStatusSuccess) {
// Request succeeded, meaning achievedAccuracy is at least the requested accuracy, and
// currentLocation contains the device's current location.
}
else if (status == INTULocationStatusTimedOut) {
// Wasn't able to locate the user with the requested accuracy within the timeout interval.
// However, currentLocation contains the best location available (if any) as of right now,
// and achievedAccuracy has info on the accuracy/recency of the location in currentLocation.
}
else {
// An error occurred, more info is available by looking at the specific status returned.
}
}];
First, create a CLLocationManager, then set your controller to its delegate, finally call [self.locationManager startUpdatingLocations]; to display the dialog.
Then use the CLLocationManagerDelegate methods to determine what the user chose:
On success,
– locationManager:didUpdateLocations:
gets called. Otherwise
– locationManager:didFailWithError:

Resources