How to force Swift to wait the previous code is finished? - swift

I just began learning Swift, I am working with an API behind, the problem is that : Swift don't wait my function finish therefore my last function appears like no code was done before.
import Foundation
import UIKit
import Alamofire
import SwiftyJSON
// Classes
class User {
init(data: Any) {
self.sex = JSON(data)["sex"].string!
print(self.sex)
}
var id: Int = 1
var online: Bool = false
var picture: String = ""
var sex: String = "Male"
}
// Fonctions
func getBackground(_ apiURL: String, completion : #escaping(_ :Any) -> Void) {
// Requête API avec Alamofire + SwiftyJSON
AF.request(apiURL, method: .get).validate().responseJSON { response in
switch response.result {
case .success(let value):
let jsonData = JSON(value)
completion(jsonData["results"])
case .failure(let error):
print(error)
}
}
}
// Requête de connexion
getBackground("https://x..me", completion: { response in
let user = User(data: response)
})
print(String(user.id) + ": " + String(user.online!))
Screenshot here
I have this error: "Use of unresolved identifier 'user'", I guess that Swift don't get the fact that User was defined previously
All my work works perfectly with my API, the "self.sex" is set and showed when I build the code.
But I'm still "locked" like I can't do nothing after this code because Swift don't want to wait.
I tried the function async and sync but then, all my next code has to be under a function...
Thanks in advance

There's no need to wait.
Put the print line – and the code which proceeds the user – in the completion closure
getBackground("https://a.clet.me", completion: { response in
let user = User(data: response)
print(String(user.id) + ": " + String(user.online!))
})
The error Use of unresolved identifier 'user' occurs because the local variable user is only visible inside the scope ({}) of its declaration.

So, this problem is all about variables life cycle.
// This happens first
getBackground("https://a.clet.me", completion: { response in
let user = User(data: response)
// This happens third, once the request has completed.
// user exists here.
// Only after this moment you should handle anything related to the user
})
// This happens second
// user DOESN'T exist here, swift will move on

Related

How to return data as a view model from alomofire coming from .net background

I am new to swift ui and am learning how to make an api call using alomofire.
I am from a csharp so bare with me if this is wrong I have a class that calls my api
My Understanding is I need a completion handler the data that is returned async so Im not sure what I need to do to extract the json properly and have a view model the following is my code
class webapilib{
var apiURLBase = "https://secreturl.com/api/"
init() {
}
func getFriends(completionHandler: ) {
performRequest()
}
func performRequest () {
let request = apiURLBase
var url = request + "Friends/GetAllParentsFriends?parentId=1"
AF.request(url).responseData { response in
switch response.result {
case .success(let value):
return String(data: value)
case .failure(let error):
print(error)
}
}
Example data
{"parents":[{"id":1,"type":null,"firstName":"Murry","groupId":null,"children":null,"emmailAddress":null,"surname":"Goldberg","name":null,"isParent":null,"isDeleted":false,"isActive":true,"createdBy":null,"lastModifiedBy":null,"lastUpdatedDate":null,"createdDate":null}],"children":[{"id":24864,"type":null,"orginizationId":null,"personId":null,"coachId":null,"teamId":null,"playerLevel":"A","fullName":"Audrey Lind","firstName":"Audrey","surname":"Lind","year":2008,"weight":null,"gender":null,"photo":null,"orgId":null,"ageGroup":null,"age":null,"emailAddress":"tandklind#msn.com","conditioningWorkouts":null,"conditioningWorkout":null,"bookings":null,"bikeWorkOuts":null,"isDeleted":false,"notes":null,"defaultTB":150.00,"defaultOP":15.00,"defaultPU":0.00,"defaultPB":null,"defaultBP":15.00,"defaultTBReps":5.00,"defaultOPReps":0.00,"defaultPUReps":10.00,"defaultPBReps":null,"defaultBPReps":0.00,"defaultAdvancedPuReps":0.00,"defaultAdvancedPu":0.00,"defaultPuSeconds":null,"defaultBroadJumpFeet":null,"defaultBroadJumpInches":null,"defaultTwentyFiveYards":null,"defaultOneFityYards":null,"status":0,"isActive":true,"createdBy":"System Import","lastModifiedBy":"Brandon","lastUpdatedDate":"2022-03-29T15:00:28.944125","createdDate":"2021-12-17T01:07:01.1819126"},{"id":24866,"type":null,"orginizationId":null,"personId":null,"coachId":null,"teamId":null,"playerLevel":"A","fullName":"Ellarae Atkinson","firstName":"Ellarae","surname":"Atkinson","year":2009,"weight":null,"gender":null,"photo":null,"orgId":null,"ageGroup":null,"age":null,"emailAddress":"atkinson_06#comcast.net","conditioningWorkouts":null,"conditioningWorkout":null,"bookings":null,"bikeWorkOuts":null,"isDeleted":false,"notes":null,"defaultTB":55.00,"defaultOP":20.00,"defaultPU":0.00,"defaultPB":null,"defaultBP":15.00,"defaultTBReps":2399.00,"defaultOPReps":0.00,"defaultPUReps":0.00,"defaultPBReps":null,"defaultBPReps":0.00,"defaultAdvancedPuReps":0.00,"defaultAdvancedPu":0.00,"defaultPuSeconds":null,"defaultBroadJumpFeet":null,"defaultBroadJumpInches":null,"defaultTwentyFiveYards":null,"defaultOneFityYards":null,"status":0,"isActive":true,"createdBy":"System Import","lastModifiedBy":null,"lastUpdatedDate":"2022-02-09T07:09:32.994522","createdDate":"2021-12-17T01:07:09.6571143"}]}
This is how am calling my from my button I eventually want this data in a list of some kind with card views
func callApi()
{
let api = thehockeylabapi().self
let test: () = api.getFriends()
}
my button click handler
Button("getFriends", action: callApi)
Again sorry for any messy code this is my first full week of learning so thanks in advance

Accessing Google API data from within 3 async callbacks and a function in SwiftUI

I know this question is asked a lot, but I can't figure out how to apply any answers to my program. Sorry in advance this async stuff makes absolutely zero sense to me.
Basically, I have a button in SwiftUI that, when pressed, calls a function that makes two API calls to Google Sheets using Alamofire and GoogleSignIn.
Button("Search") {
if fullName != "" {
print(SheetsAPI.nameSearch(name: fullName, user: vm.getUser()) ?? "Error")
}
}
This function should return the values of some cells on success or nil on an error. However, it only ever prints out "Error". Here is the function code.
static func nameSearch<S: StringProtocol>(name: S, advisory: S = "", user: GIDGoogleUser?) -> [String]? {
let name = String(name)
let advisory = String(advisory)
let writeRange = "'App Control'!A2:C2"
let readRange = "'App Control'!A4:V4"
// This function can only ever run when user is logged in, ! should be fine?
let user = user!
let parameters: [String: Any] = [
"range": writeRange,
"values": [
[
name,
nil,
advisory
]
]
]
// What I want to be returned
var data: [String]?
// Google Identity said use this wrapper so that the OAuth tokens refresh
user.authentication.do { authentication, error in
guard error == nil else { return }
guard let authentication = authentication else { return }
// Get the access token to attach it to a REST or gRPC request.
let token = authentication.accessToken
let headers: HTTPHeaders = ["Authorization": "Bearer \(token)"]
AF.request("url", method: .put, parameters: parameters, encoding: JSONEncoding.default, headers: headers).responseString { response in
switch response.result {
case .success:
// I assume there is a better way to make two API calls...
AF.request("anotherURL", headers: headers).responseDecodable(of: NameResponseModel.self) { response2 in
switch response2.result {
case .success:
guard let responseData = response2.value else { return }
data = responseData.values[0]
// print(responseData.values[0]) works fine
case .failure:
print(response2.error ?? "Unknown error.")
data = nil
}
}
case .failure:
print(response.error ?? "Unknown error.")
data = nil
}
}
}
// Always returns nil, "Unknown error." never printed
return data
}
The model struct for my second AF request:
struct NameResponseModel: Decodable { let values: [[String]] }
An example API response for the second AF request:
{
"range": "'App Control'!A4:V4",
"majorDimension": "ROWS",
"values": [
[
"Bob Jones",
"A1234",
"Cathy Jones",
"1234 N. Street St. City, State 12345"
]
]
}
I saw stuff about your own callback function as a function parameter (or something along those lines) to handle this, but I was completely lost. I also looked at Swift async/await, but I don't know how that works with callback functions. Xcode had the option to refactor user.authentication.do { authentication, error in to let authentication = try await user.authentication.do(), but it threw a missing parameter error (the closure it previously had).
EDIT: user.authentication.do also returns void--another reason the refactor didn't work (I think).
There is probably a much more elegant way to do all of this so excuse the possibly atrocious way I did it.
Here is the link to Google Identity Wrapper info.
Thanks in advance for your help.
Solved my own problem.
It appears (according to Apple's async/await intro video) that when you have an unsupported callback that you need to run asynchronously, you wrap it in something called a Continuation, which allows you to manually resume the function on the thread, whether throwing or returning.
So using that code allows you to run the Google Identity token refresh with async/await.
private static func auth(_ user: GIDGoogleUser) async throws -> GIDAuthentication? {
typealias AuthContinuation = CheckedContinuation<GIDAuthentication?, Error>
return try await withCheckedThrowingContinuation { (continuation: AuthContinuation) in
user.authentication.do { authentication, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: authentication)
}
}
}
}
static func search(user: GIDGoogleUser) async throws {
// some code
guard let authentication = try await auth(user) else { ... }
// some code
}
I then ran that before using Alamofire's built-in async/await functionality for each request (here's one).
let dataTask = AF.request(...).serializingDecodable(NameResponseModel.self)
let response = try await dataTask.value
return response.values[0]

How to call function after async requests finish in SwiftUI?

I have a function that calls 2 types of api requests to get a bunch of data i need in my app. In the function I make a request for locations, then for each location in the response I make a different request to get details of that specific location. (ex. if request 1 returns 20 locations, my second request is called 20 times, once for each location)
My function code here:
func requestAndCombineGData(location: CLLocation, radius: Int) {
// Clears map of markers
self.mapView.clear()
// Calls 'Nearby Search' request
googleClient.getGooglePlacesData(location: location, withinMeters: radius) { (response) in
print("Made Nearby Search request. Returned response here:", response)
// loops through each result from the above Nearby Request response
for location in response.results {
// Calls 'Place Details' request
self.googleClient.getGooglePlacesDetailsData(place_id: location.place_id) { (detailsResponse) in
print("GMV returned - detailsResponse.result - ", detailsResponse.result)
}
}
}
}
Request functions I reference above are here:
func getGooglePlacesData(location: CLLocation, withinMeters radius: Int, using completionHandler: #escaping (GooglePlacesResponse) -> ()) {
for category in categoriesArray {
let url = googlePlacesNearbyDataURL(forKey: googlePlacesKey, location: location, radius: radius, type: category)
let task = session.dataTask(with: url) { (responseData, _, error) in
if let error = error {
print(error.localizedDescription)
return
}
guard let data = responseData, let response = try? JSONDecoder().decode(GooglePlacesResponse.self, from: data) else {
print("Could not decode JSON response")
completionHandler(GooglePlacesResponse(results:[]))
return
}
if response.results.isEmpty {
print("GC - response returned empty", response)
} else {
print("GC - response contained content", response)
completionHandler(response)
}
}
task.resume()
}
}
func getGooglePlacesDetailsData(place_id: String, using completionHandler: #escaping (GooglePlacesDetailsResponse) -> ()) {
let url = googlePlacesDetailsURL(forKey: googlePlacesKey, place_ID: place_id)
let task = session.dataTask(with: url) { (responseData, _, error) in
if let error = error {
print(error.localizedDescription)
return
}
guard let data = responseData, let detailsResponse = try? JSONDecoder().decode(GooglePlacesDetailsResponse.self, from: data) else {
print("Could not decode JSON response. responseData was: ", responseData)
return
}
print("GPD response - detailsResponse.result: ", detailsResponse.result)
completionHandler(detailsResponse)
}
task.resume()
}
After I get all the data im requesting (or even as the data is coming in) I would like to append it to an #EnvironmentObject (array) I have set up in my SceneDelegate.swift file. Im using the data in multiple places in my app so the #EnvironmentObject serves as a 'source of truth'.
I tried accomplishing this using the code below, but keep getting the error - "Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates."
func requestAndCombineGData(location: CLLocation, radius: Int) {
// Clears map of markers
self.mapView.clear()
// Calls 'Nearby Search' request
googleClient.getGooglePlacesData(location: location, withinMeters: radius) { (response) in
print("Made Nearby Search request. Returned response here:", response)
// loops through each result from the above Nearby Request response
for location in response.results {
// Calls 'Place Details' request
self.googleClient.getGooglePlacesDetailsData(place_id: location.place_id) { (detailsResponse) in
print("GMV returned - detailsResponse.result - ", detailsResponse.result)
// THIS IS WHERE I TRY TO UPDATE MY #ENVIROMETOBJECT
self.venueData.venuesdataarray.append(detailsRespose.result)
}
}
}
}
I believe I need to make sure the requests complete THEN try to update my #EnvironmentObject, but I do not know how to do that.
EDIT - providing my VenueData struct as requested in comments:
struct VenueData : Identifiable {
var id = UUID()
var name : String
var geometry : Location?
var rating : String?
var price_level : String?
var types : [String]?
var formatted_address : String?
var formatted_phone_number : String?
var website : String?
var photo_reference : String?
enum CodingKeysDetails : String, CodingKey {
case geometry = "geometry"
case name = "name"
case rating = "rating"
case price_level = "price_level"
case types = "types"
case opening_hours = "opening_hours"
case formatted_address = "formatted_address"
case formatted_phone_number = "formatted_phone_number"
case website = "website"
}
// Location struct
struct Location : Codable {
var location : LatLong
enum CodingKeys : String, CodingKey {
case location = "location"
}
// LatLong struct
struct LatLong : Codable {
var latitude : Double
var longitude : Double
enum CodingKeys : String, CodingKey {
case latitude = "lat"
case longitude = "lng"
}
}
}
}
class VenueDataArray: ObservableObject {
#Published var venuesdataarray : [VenueData] = [
VenueData(name: "test_name")
]
}
Solution Edit - I tried using this snippet of code within my second api request and it solved my issue, although i do not understand why I need to do this
DispatchQueue.main.async {
self.venueData.venuesdataarray.append(RESPONSE_DETAILS_HERE)
}
Originally I had asked, Does anyone know how I can update my #EnvironmentObject after all the requests complete?
Does anyone know why the snippet I have above makes everything work?
Id just like to understand what im doing and maybe someone could learn something if they find this
I tried using this snippet of code within my second api request and it solved my issue, although i do not understand why I need to do this
DispatchQueue.main.async {
self.venueData.venuesdataarray.append(RESPONSE_DETAILS_HERE)
}
Originally I had asked, Does anyone know how I can update my #EnvironmentObject after all the requests complete?
Does anyone know why the snippet I have above makes everything work? Id just like to understand what im doing and maybe someone could learn something if they find this
There are several things that you cannot successfully do from a background thread. Some of them (like UIKit content changes) do not generate an error, but fail silently which is worse. You have the good fortune to have received a relatively specific error message.
The error message was that you couldn't publish changes from a background thread and needed to do that from the main thread.
Wrapping your append inside "DispatchQueue.main.async" makes that line of code run on the main thread.
That's it.
This could probably have been explained more concisely.

How to correct the order of execution of code in Swift 5?

The code within the function is executed in a different order than it is expected. I wanted to change the state of the login Boolean variable inside the if statement, but the function returns the initial value before if statement is completed.
Code sample:
class ClassName {
func loginRequest (name: String, pwd: String) -> Bool {
var login:Bool
//Initial value for login
login = false
let task = session.uploadTask(with: request, from: jsonData) { data, response, error in
if let httpResponse = response as? HTTPURLResponse {
print(httpResponse.statusCode)
if (httpResponse.statusCode) == 200 {
//Change the value of login if login is successful
login = true
if let data = data, let dataString = String(data: data, encoding: .utf8) {
do {
...
} catch {print(error.localizedDescription)}
}
}
}
}
task.resume()
//Problem return false in any case because return is completed before if statement
return login
}
}
Completion Handlers is your friend
The moment your code runs task.resume(), it will run your uploadTask and only when that function is finished running it will run the code where you change your login variable.
With That said: That piece of code is running asynchronously. That means your return login line of code won't wait for your network request to come back before it runs.
Your code is actually running in the order it should. But i myself wrote my first network call like that and had the same problem. Completion Handles is how i fixed it
Here is a very nice tutorial on Completion Handlers or you might know it as Callbacks :
Link To Completion Handlers Tutorial
If i can give you a little hint - You will have to change your function so it looks something like this: func loginRequest (name: String, pwd: String, completionHandler: #escaping (Bool) -> Void)
And replace this login = true with completionHandler(true)
Wherever it is you call your function it will look something like this:
loginRequest(name: String, pwd: String) {didLogIn in
print("Logged In : \(didLogIn)")
}
One last thing... You're actually already using Completion Handlers in your code.
let task = session.uploadTask(with: request, from: jsonData) { data, response, error in
... ... But hopefully now you understand a little bit better, and will use a completion handler approach when making network calls.
GOOD LUCK !

Struggling with Vapor Client

I'm trying to make a simple get request to Google Places API from my vapor web service.
This is what my controller looks like:
import Vapor
import HTTP
import VaporPostgreSQL
final class MainController {
var currentDroplet: Droplet!
func addRoutes(drop: Droplet) {
currentDroplet = drop
drop.get("places",String.self, String.self, handler: getNearbyPlaces)
}
func getNearbyPlaces(request: Request, lat: String, long: String) throws -> ResponseRepresentable {
let googleAPIKey = "MY_KEY"
let googlePlacesBaseURL = "https://maps.googleapis.com/maps/api/place/nearbysearch"
let url = googlePlacesBaseURL + "/json?location=\(lat),\(long)&radius=500&types=food&key=" + googleAPIKey
print(url)
let apiResponse = try drop.client.get(url)
print(apiResponse)
return apiResponse.json != nil ? apiResponse.json! : "Something went bad"
}
}
It should be as simple as that, however when I call it, the request keeps hanging for a long time and then it returns 500.
Note that the printed url in the console does work fine directly in the browser.
I couldn't figure out a useful way to catch and debug any errors too.
I needed to add import Foundation and drop.client = FoundationClient.self to main.swift to get a similar call to work.