Convert recursive async function to promise - swift

I have a recursive, async function that queries Google Drive for a file ID using the REST api and a completion handler:
func queryForFileId(query: GTLRDriveQuery_FilesList,
handler: #escaping FileIdCompletionHandler) {
service.executeQuery(query) { ticket, data, error in
if let error = error {
handler(nil, error)
} else {
let list = data as! GTLRDrive_FileList
if let pageToken = list.nextPageToken {
query.pageToken = pageToken
self.queryForFileId(query: query, handler: handler)
} else if let id = list.files?.first?.identifier {
handler(id, nil)
} else {
handler(nil, nil) // no file found
}
}
}
}
Here, query is set up to return the nextPageToken and files(id) fields, service is an instance of GTLRDriveService, and FileIdCompletionHandler is just a typealias:
typealias FileIdCompletionHandler = (String?, Error?) -> Void
I've read how to convert async functions into promises (as in this thread) but I don't see how that can be applied to a recursive, async function. I guess I can just wrap the entire method as a Promise:
private func fileIdPromise(query: GTLRDriveQuery_FilesList) -> Promise<String?> {
return Promise { fulfill, reject in
queryForFileId(query: query) { id, error in
if let error = error {
reject(error)
} else {
fulfill(id)
}
}
}
}
However, I was hoping to something a little more direct:
private func queryForFileId2(query: GTLRDriveQuery_FilesList) -> Promise<String?> {
return Promise { fulfill, reject in
service.executeQuery(query) { ticket, data, error in
if let error = error {
reject(error)
} else {
let list = data as! GTLRDrive_FileList
if let pageToken = list.nextPageToken {
query.pageToken = pageToken
// WHAT DO I DO HERE?
} else if let id = list.files?.first?.identifier {
fulfill(id)
} else {
fulfill(nil) // no file found
}
}
}
}
}
So: what would I do when I need to make another async call to executeQuery?

If you want to satisfy a recursive set of promises, at where your "WHAT DO I DO HERE?" line, you'd create a new promise.then {...}.else {...} pattern, calling fulfill in the then clause and reject in the else clause. Obviously, if no recursive call was needed, though, you'd just fulfill directly.
I don't know the Google API and you didn't share your code for satisfying a promise for a list of files, so I'll have to keep this answer a bit generic: Let's assume you had some retrieveTokens routine that returned a promise that is satisfied only when all of the promises for the all files was done. Let's imagine that the top level call was something like:
retrieveTokens(for: files).then { tokens in
print(tokens)
}.catch { error in
print(error)
}
You'd then have a retrieveTokens that returns a promise that is satisfied only when then promises for the individual files were satisfied. If you were dealing with a simple array of File objects, you might do something like:
func retrieveTokens(for files: [File]) -> Promise<[Any]> {
var fileGenerator = files.makeIterator()
let generator = AnyIterator<Promise<Any>> {
guard let file = fileGenerator.next() else { return nil }
return self.retrieveToken(for: file)
}
return when(fulfilled: generator, concurrently: 1)
}
(I know this isn't what yours looks like, but I need this framework to show my answer to your question below. But it’s useful to encapsulate this “return all promises at a given level” in a single function, as it allows you to keep the recursive code somewhat elegant, without repeating code.)
Then the routine that returns a promise for an individual file would see if a recursive set of promises needed to be returned, and put its fulfill inside the then clause of that new recursively created promise:
func retrieveToken(for file: File) -> Promise<Any> {
return Promise<Any> { fulfill, reject in
service.determineToken(for: file) { token, error in
// if any error, reject
guard let token = token, error == nil else {
reject(error ?? FileError.someError)
return
}
// if I don't have to make recursive call, `fulfill` immediately.
// in my example, I'm going to see if there are subfiles, and if not, `fulfill` immediately.
guard let subfiles = file.subfiles else {
fulfill(token)
return
}
// if I got here, there are subfiles and I'm going to start recursive set of promises
self.retrieveTokens(for: subfiles).then { tokens in
fulfill(tokens)
}.catch { error in
reject(error)
}
}
}
}
Again, I know that the above isn't a direct answer to your question (as I'm not familiar with Google Drive API nor how you did your top level promise logic). So, in my example, I created model objects sufficient for the purposes of the demonstration.
But hopefully it's enough to illustrate the idea behind a recursive set of promises.

Related

swift function doesnt return a value [duplicate]

This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed last year.
I'm new at Swift and that's why i need your help. So I have a function which should send request and return a value
func getAnswer() -> String? {
var answer: String?
guard let url = URL(string: "https://8ball.delegator.com/magic/JSON/_") else { return nil }
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {
return
}
guard let response = response as? HTTPURLResponse else { return }
guard response.statusCode == 200 else { return }
do {
let model = try JSONDecoder().decode(Answer.self, from: data)
DispatchQueue.main.async {
answer = model.magic.answer
}
} catch let error {
fatalError(error.localizedDescription)
}
}.resume()
return answer
}
but it always returns nil.
I suppose problem is here
DispatchQueue.main.async {
answer = model.magic.answer
}
How can I fix it?
In order to know what is happening here, you need to learn about #escaping functions in swift, here is some link1 together with taking function as another functions parameter link2 written in part "Function Types as Parameter Types" , closures in Swift link3 and
Here is what is happening simplified and explained step by step :
you call getAnswer()
variable answer gets initialized with value nil by declaring answer: String?
URLSession.shared.dataTask is called and it is taking as an argument another function - closure (Data?, URLResponse?, Error?) -> Void . Also URLSession.shared.dataTask is executed on different thread and is not returning yet, but will return right after it receives response from server, which can take any time (but usually milliseconds) and will basically happen after your getAnswer() function is returning value.
your getAnswer() immediately returns value of answer which is currently nil
if you get any data from server, or server could not be reached, your URLSession.shared.dataTask function executes your code in closure. This is the code it will execute:
guard let data = data, error == nil else {
return
}
guard let response = response as? HTTPURLResponse else { return }
guard response.statusCode == 200 else { return }
do {
let model = try JSONDecoder().decode(Answer.self, from: data)
DispatchQueue.main.async {
answer = model.magic.answer
}
} catch let error {
fatalError(error.localizedDescription)
}
Your problem lies in how swift executes closures. When you call
URLSession.shared.dataTask(with: url) {
// Closure code here
}
return answer
Your "Closure code here" doesn't get called until the endpoint "https://8ball.delegator.com/magic/JSON/_" actually gives a response. However, you've promised swift that your function will return an optional string immediately after the serial code of your function has completed. For this reason, by the time your "Closure code here" has run, and your "answer" variable has been updated with the correct value, your function is long gone, and has already returned a value (which in this case is whatever you've set it to at the beginning - nil).
You can fix this issue in one of two ways.
Swift's new concurrency system
By defining your own closure.
Swift's new concurrency system
You can define your function as async, meaning that the function won't have to return a value in serial, as follows.
enum GetAnswerError: Error {
case invalidURL
}
func getAnswer() async throws -> String {
var answer: String?
guard let url = URL(string: "https://8ball.delegator.com/magic/JSON/_") else {
throw GetAnswerError.invalidURL
}
// Your function will suspend here and probably be moved to a different thread. It will resume once a response has been received from the endpoint.
let (data, _) = try await URLSession.shared.dataTask(with: url)
let parsedData = try JSONDecoder().decode(Answer.self, from: data)
return parsedData.magic.answer
}
When you call this function, you'll have to do so from an environment which swift can suspend. This means you'll call the function from either another async function like so
func anotherFunction() async throws -> Bool {
let answer = try await getAnswer()
// Run some code here
return answer == "YES" // Return some useful value
}
or from a Task object like so
Task {
// Note that because the function getAnswer() can throw errors, you'll have to handle them when you call the function. In this case, I'm handling them by using try?, which will simply set answer to nil if an error is thrown.
let answer = try? await getAnswer()
}
Note that when you call code in a task, you must be using the return value's from within the scope of the task. If you try to do something like this
func getAnswerTheSecond() -> String? {
var answer: String? = nil
Task {
let receivedAnswer = try? await getAnswer()
answer = receivedAnswer
}
return answer
}
You'll just end up back where you started, where swift immediately returns the nil value because your code is ran in serial. To fix this, run the relevant code on the "answer" from wherever it is needed within the task. If you are using the "answer" to update a SwiftUI view that might look like this.
struct ContentView: View {
#State var answer: String = ""
// This is the function that I've written earlier
func getAnswer() async throws -> String {
// Make URL Request
// Return the value
}
var body: some View {
Text(self.answer)
.onAppear{
Task{
let result = try? await self.getAnswer()
self.answer = result
}
}
}
}
Defining your own closure
You can define your own closure to handle the URL response; however, because of swift's new concurrency framework, this is probably not the right way to go.
If you'd like to go this way, do a google search for "Swift closures", and you'll find what you need.

Why am I still able to fetch data, even with deleting FireStore object in Swift?

I deleted an entry in the Firestore and also checked it manually to confirm that. However, as long as I do not close the application, I can send a request to fetch the data and I still get the result. This should not be the case.
If you imagine having a shared photo with some textual information and you delete those information, this would mean, other users can still see the textual information (fetched from the Firestore) but not the image anymore (store in Firestorage).
I want to display a message on the UI, something like "The content does not exist anymore".
How I can achieve that? I used the following approach so far but it does not work at the moment:
public func checkIfChallengeObjectExists(completionHandler:#escaping(Bool)->(), challengeId:String) {
CHALLENGE_COLLECTION?.document(challengeId).getDocument(completion: { (querySnapshot, error) in
if (error != nil) {
print(error?.localizedDescription as Any)
}
if (querySnapshot?.documentID == "" || querySnapshot!.metadata.isFromCache) {
completionHandler(false)
}
else {
completionHandler(true)
}
})
}
Any solutions?
Non-existent documents will still return document snapshots, but they will be empty. Therefore, you must check the contents of the snapshot for the document, not the snapshot itself. Also, you should handle errors and the overall flow of the return better.
public func checkIfChallengeObjectExists(completionHandler:#escaping(Bool)->(), challengeId:String) {
CHALLENGE_COLLECTION?.document(challengeId).getDocument(completion: { (querySnapshot, error) in
if let doc = querySnapshot,
doc.exists {
completionHandler(true) // only one possible true condition
} else {
if let error = error {
print(error.localizedDescription)
}
completionHandler(false) // all else false
}
})
}
As a side note, I recommend reordering the parameters of the function to make it easier to read when called (conventionally, the completion handler comes last) and giving the boolean argument a name so it's easier to read when referencing (sometime later or by other developers).
public func verifyChallengeObject(ID: String, _ completion: #escaping (_ exists: Bool) -> Void) {
...
}
verifyChallengeObject(ID: "abc123", { (exists) in
if exists {
...
} else {
...
}
})

Querying and mapping using Vapor

I'm trying to write a function using Swift and Vapor but I don't understand why one statement gets printed before the other:
// Logout user
func logout(_ req: Request) throws -> Future<APIResponseMessage> {
let userID = self.checkAccessToken(req: req)
// Delete access token here
let apiResponseMessage = APIResponseMessage()
apiResponseMessage.message = "success"
apiResponseMessage.userID = userID
return apiResponseMessage.create(on: req)
}
func checkAccessToken(req: Request) -> Int {
let bearerAuthorization = req.http.headers.bearerAuthorization
guard let _bearerAuthorization = bearerAuthorization else {
// Works fine
print("no bearer incluced")
return 0
}
let _ = AccessToken.query(on: req).filter(\.accessToken == _bearerAuthorization.token).first().map(to: Int.self) { queriedAccessToken in
// This should be first
print("This gets printed second")
return queriedAccessToken!.userID!
}
// This should be second
print("This gets printed first")
return 0
}
Can anyone tell me how to make the second print statement wait until the first one is completed?
Right now it's causing my logout function to run with userID == 0 when this shouldn't be the case
As #nathan said, this is due to your code being async. Your .map callback is like the closure you pass into to a URLSession.dataTask when making request's to an external API for an iOS app.
Vapor uses a slightly different async model then what you use in iOS though, using promises and futures instead of callback closures. You can read about them in the docs.
In your case, you want to return the userID you get from the AccessToken query. To do this, you first need to change your method's return type from Int to Future<Int>. Then, instead of assigning the result of the .map call to _, you can return it from the method:
func checkAccessToken(req: Request) -> Future<Int> {
let bearerAuthorization = req.http.headers.bearerAuthorization
guard let _bearerAuthorization = bearerAuthorization else {
return req.future(0)
}
return AccessToken.query(on: req).filter(\.accessToken == _bearerAuthorization.token).first().map(to: Int.self) { queriedAccessToken in
return queriedAccessToken!.userID!
}
}
I would suggest you look into error handling for your queriedAccessToken and userID values so you aren't force-unwrapping them.

Closures for waiting data from CloudKit

I have a CloudKit database with some data. By pressing a button my app should check for existence of some data in the Database. The problem is that all processes end before my app get the results of its search. I found this useful Answer, where it is said to use Closures.
I tried to follow the same structure but Swift asks me for parameters and I get lost very quick here.
Does someone can please help me? Thanks for any help
func reloadTable() {
self.timePickerView.reloadAllComponents()
}
func getDataFromCloud(completionHandler: #escaping (_ records: [CKRecord]) -> Void) {
print("I begin asking process")
var listOfDates: [CKRecord] = []
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Riservazioni", predicate: predicate)
let queryOperation = CKQueryOperation(query: query)
queryOperation.resultsLimit = 20
queryOperation.recordFetchedBlock = { record in
listOfDates.append(record)
}
queryOperation.queryCompletionBlock = { cursor, error in
if error != nil {
print("error")
print(error!.localizedDescription)
} else {
print("NO error")
self.Array = listOfDates
completionHandler(listOfDates)
}
}
}
var Array = [CKRecord]()
func generateHourArray() {
print("generate array")
for hour in disponibleHours {
let instance = CKRecord(recordType: orderNumber+hour)
if Array.contains(instance) {
disponibleHours.remove(at: disponibleHours.index(of: hour)!)
}
}
}
func loadData() {
timePickerView.reloadAllComponents()
timePickerView.isHidden = false
}
#IBAction func checkDisponibility(_ sender: Any) {
if self.timePickerView.isHidden == true {
getDataFromCloud{ (records) in
print("gotData")
self.generateHourArray()
self.loadData()
}
print(Array)
}
}
Im struggling to understand your code and where the CloudKit elements fit in to it, so Im going to try and give a generic answer which will hopefully still help you.
Lets start with the function we are going to call to get our CloudKit data, lets say we are fetching a list of people.
func getPeople() {
}
This is simple enough so far, so now lets add the CloudKit code.
func getPeople() {
var listOfPeople: [CKRecord] = [] // A place to store the items as we get them
let query = CKQuery(recordType: "Person", predicate: NSPredicate(value: true))
let queryOperation = CKQueryOperation(query: query)
queryOperation.resultsLimit = 20
// As we get each record, lets store them in the array
queryOperation.recordFetchedBlock = { record in
listOfPeople.append(record)
}
// Have another closure for when the download is complete
queryOperation.queryCompletionBlock = { cursor, error in
if error != nil {
print(error!.localizedDescription)
} else {
// We are done, we will come back to this
}
}
}
Now we have our list of people, but we want to return this once CloudKit is done. As you rightly said, we want to use a closure for this. Lets add one to the function definition.
func getPeople(completionHandler: #escaping (_ records: [CKRecord]) -> Void) {
...
}
This above adds a completion hander closure. The parameters that we are going to pass to the caller are the records, so we add that into the definition. We dont expect anyone to respond to our completion handler, so we expect a return value of Void. You may want a boolean value here as a success message, but this is entirely project dependent.
Now lets tie the whole thing together. On the line I said we would come back to, you can now replace the comment with:
completionHandler(listOfPeople)
This will then send the list of people to the caller as soon as CloudKit is finished. Ive shown an example below of someone calling this function.
getPeople { (records) in
// This code wont run until cloudkit is finished fetching the data!
}
Something to bare in mind, is which thread the CloudKit API runs on. If it runs on a background thread, then the callback will also be on the background thread - so make sure you don't do any UI changes in the completion handler (or move it to the main thread).
There are lots of improvements you could make to this code, and adapt it to your own project, but it should give you a start. Right off the bat, Id image you will want to change the completion handler parameters to a Bool to show whether the data is present or not.
Let me know if you notice any mistakes, or need a little more help.

How to catch errors from two requests in one place using RxSwift

I'm quite new and I'm wondering how to catch error from requests which are zipped (see snipped above) in one place. In current implementation I have error handling in two places, but my goal is to have it in one place. My requests are zipped because if one of this req gets failed whole sequence will fail so in result I want to have one error handling place in code for both request.
let firstReq = self.sendReq() // returns Observable<Bool>
.catchError {
error in
return self.just(true)
}
let secondReq = self.sendReqTwo() // returns Observable<Bool>
.catchError {
error in
return self.just(true)
}
goBttnOutlet.rx_tap
.subscribeNext {
Observable.zip(firstReqRes, secondReqRes) { (firstRes, secondRes) -> Bool in
return firstRes && secondRes
}.subscribeNext { summaryRes in
print("🎿 \(summaryRes)")
}.addDisposableTo(self.rx_disposableBag)
}.addDisposableTo(rx_disposableBag)
..maybe some link with example code with handling error in common place will be great for me.
Thanks a lot.
zip returns a new Observable<T>, so you can simply move the catchError operator application to what zip returns.
let firstReq = self.sendReq()
let secondReq = self.sendReqTwo()
let zippedReq = Observable.zip(firstReq, secondReq)
.catchErrorJustReturn { _ in true }
goBttnOutlet.rx_tap
.subscribeNext {
zippedReq.subscribeNext { summaryRes in
print("🎿 \(summaryRes)")
}.addDisposableTo(self.rx_disposableBag)
}.addDisposableTo(rx_disposableBag)
On a side note, you could improve the chain after goBttnOutlet to the following
goBttnOutlet.rx_tap.flatMap { zippedReq }
.subscribeNext { summaryRes in
print("🎿 \(summaryRes)")
}.addDisposableTo(rx_disposableBag)
See flatMap documentation for details.