Difficulty with Swift/Alamofire Completion Handlers - ios

Today I am using Alamofire to extract an int value.
It's pulling the int fine, but I want to save the int value outside of the request (in a different function, viewDidLoad()).
I'm not sure how to call the completion handler, it doesn't seem to be doing anything at the moment.
Here is the method that extracts the int value in question (lastModified).
func checkVersion(completion: #escaping (Int) -> Void) {
Alamofire.request(versionCheckEndpoint).responseJSON { response in
if(response.result.value != nil) {
let json = JSON(response.result.value!)
self.s3Endpoint = json["s3BucketURL"].stringValue
let lastModified = json["lastModified"].intValue
self.latestVersion = lastModified
}
}
}
I tried saving the value to an instance variable, this doesn't work.
I called the completion handler like this in viewDidLoad().. but I'm positive it's nowhere near correct:
self.checkVersion() { response in
self.latestVersion = response
}
Can anyone give me some insight on how I can save the int value for lastModified to some local variable so it is not contained within the scope of the Alamofire request?

In the function checkVersion the completion block isn't called.
You must call it after retrieving the value (although I would suggest to make it optional)
func checkVersion(_ completion: #escaping (Int?) -> Void) {
Alamofire.request(versionCheckEndpoint).responseJSON { response in
guard let value = response.result.value else {
completion(nil)
return
}
let json = JSON(value)
self.s3Endpoint = json["s3BucketURL"].stringValue
let lastModified = json["lastModified"].intValue
completion(lastModified)
}
}
And call it like this:
self.checkVersion() { value in
guard let value = value else {
//handle invalid values here
return
}
latestVersion = value
// update the view if needed
}

Related

Unexpected non-void return value in void function Swift3

I have a function that returns either a class object or nil. The function's purpose is to check if a Chat exists. The chat ID's are stored in MySQL. If the ID exists, I perform a Firebase reference to get a snapshot and then get the object. If the ID does not exist, I return nil:
func findChat(string: String) -> Chat? {
var returnValue: (Chat?)
let url = getChatsURL
let Parameters = [ "title" : string ]
Alamofire.request("\(url)", method: .post, parameters: Parameters).validate().responseString { response in
if let anyResponse = response.result.value {
self.responseFromServer = anyResponse
}
if self.responseFromServer == "" {
returnValue = nil
} else {
let ref = DatabaseReference.chats.reference()
let query = ref.queryOrdered(byChild: "uid").queryEqual(toValue: (self.responseFromServer))
query.observe(.childAdded, with: { snapshot in
returnValue = Chat(dictionary: snapshot.value as! [String : Any])
})
}
return returnValue
}
}
However, at return returnValue I am getting
Unexpected non-void return value in void function.
Any thoughts of what I could be missing?
The problem is that you are trying to return a non-void value from inside a closure, which only returns from the closure, but since that closure expects a void return value, you receive the error.
You cannot return from an asynchronous function using the standard return ... syntax, you have to declare your function to accept a completion handler and return the value from the async network call inside the completion handler.
func findChat(string: String, completion: #escaping (Chat?)->()) {
var returnValue: (Chat?)
let url = getChatsURL
let Parameters = [ "title" : string ]
Alamofire.request("\(url)", method: .post, parameters: Parameters).validate().responseString { response in
if let anyResponse = response.result.value {
self.responseFromServer = anyResponse
}
if self.responseFromServer == "" {
completion(nil)
} else {
let ref = DatabaseReference.chats.reference()
let query = ref.queryOrdered(byChild: "uid").queryEqual(toValue: (self.responseFromServer))
query.observe(.childAdded, with: { snapshot in
completion(Chat(dictionary: snapshot.value as! [String : Any]))
})
}
}
}
Then you can call this function and use the return value like this:
findChat(string: "inputString", completion: { chat in
if let chat = chat {
//use the return value
} else {
//handle nil response
}
})
Your block is executed asynchronously, but you're trying to return a value from the enclosing function. It doesn't work that way. Your findChat function needs to take a completion block itself instead of returning a value, and then you can call that completion block from the point where you're trying to say return returnValue.

Extracting data from API (JSON format) doesn't save data outside of function call

I am trying to get an array of temperatures in a given time period from an API in JSON format. I was able to retrieve the array through a completion handler but I can't save it to another variable outside the function call (one that uses completion handler). Here is my code. Please see the commented area.
class WeatherGetter {
func getWeather(_ zip: String, startdate: String, enddate: String, completion: #escaping (([[Double]]) -> Void)) {
// This is a pretty simple networking task, so the shared session will do.
let session = URLSession.shared
let string = "api address"
let url = URL(string: string)
var weatherRequestURL = URLRequest(url:url! as URL)
weatherRequestURL.httpMethod = "GET"
// The data task retrieves the data.
let dataTask = session.dataTask(with: weatherRequestURL) {
(data, response, error) -> Void in
if let error = error {
// Case 1: Error
// We got some kind of error while trying to get data from the server.
print("Error:\n\(error)")
}
else {
// Case 2: Success
// We got a response from the server!
do {
var temps = [Double]()
var winds = [Double]()
let weather = try JSON(data: data!)
let conditions1 = weather["data"]
let conditions2 = conditions1["weather"]
let count = conditions2.count
for i in 0...count-1 {
let conditions3 = conditions2[i]
let conditions4 = conditions3["hourly"]
let count2 = conditions4.count
for j in 0...count2-1 {
let conditions5 = conditions4[j]
let tempF = conditions5["tempF"].doubleValue
let windspeed = conditions5["windspeedKmph"].doubleValue
temps.append(tempF)
winds.append(windspeed)
}
}
completion([temps, winds])
}
catch let jsonError as NSError {
// An error occurred while trying to convert the data into a Swift dictionary.
print("JSON error description: \(jsonError.description)")
}
}
}
// The data task is set up...launch it!
dataTask.resume()
}
}
I am calling this method from my view controller class. Here is the code.
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30") { (weatherhandler: [[Double]]) in
//It prints out the correct array here
print(weatherhandler[0])
weatherData = weatherhandler[0]
}
//Here it prints out an empty array
print(weatherData)
}
The issue is that API takes some time to return the data, when the data is return the "Completion Listener" is called and it goes inside the "getWeather" method implementation, where it prints the data of array. But when your outside print method is called the API hasn't returned the data yet. So it shows empty array. If you will try to print the data form "weatherData" object after sometime it will work.
The best way I can suggest you is to update your UI with the data inside the "getWeather" method implementation like this:
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30") { (weatherhandler: [[Double]]) in
//It prints out the correct array here
print(weatherhandler[0])
weatherData = weatherhandler[0]
// Update your UI here.
}
//Here it prints out an empty array
print(weatherData)
}
It isn't an error, when your controller get loaded the array is still empty because your getWeather is still doing its thing (meaning accessing the api, decode the json) when it finishes the callback will have data to return to your controller.
For example if you were using a tableView, you will have reloadData() to refresh the UI, after you assign data to weatherData
Or you could place a property Observer as you declaring your weatherData property.
var weatherData:[Double]? = nil {
didSet {
guard let data = weatherData else { return }
// now you could do soemthing with the data, to populate your UI
}
}
now after the data is assigned to wheaterData, didSet will be called.
Hope that helps, and also place your jsonParsing logic into a `struct :)

Swift networking with non-void completion handler gets error

My goal is to set up a completion handler using a standard Moya request call.
Here is my process:
Call backend with with a MoyaProvider that conforms to my own BackendAPI (already set up)
Wrap this call in a completion handler to return [Player] data (Player is a custom class in the project)
Display [Player] data
Here is the actual code:
func getPlayers(orchestraId: String, finished: #escaping () -> [Player]) {
let provider = MoyaProvider<BackendAPI>()
provider.request(.getPlayers(orchestraId: orchestraId)) { (result) in
switch result {
case let .success(moyaResponse):
let statusCode = moyaResponse.statusCode
if statusCode == 200 {
let data = moyaResponse.data
let json = JSON.init(data: data)
let players: [Player] = self.deserializeJSONPlayers(with: json)
return players
} else {
print ("Non 200 for league players data")
self.debugStatementsFromResponse(response: moyaResponse)
}
case let .failure(error):
print ("Error: \(error)")
}
}
}
I am getting an error on the return line, with the note that Unexpected non-void return in void function. However, I have declared my function to be a non-void function. What I am doing incorrectly in structuring my method?
You have the completionHandler which you should use if you want to return the value with the current syntax. If you want to use return players then you must change the syntax.
Use this syntax instead to make your current code work with the completionHandler:
func getPlayers(orchestraId: String, finished: #escaping ([Player]) -> Void) {
finished(players) // Instead of return players
}

Swift 3.0 Error: Escaping closures can only capture inout parameters explicitly by value

I'm trying to update my project to Swift 3.0 but I have some difficulties.
I'm getting next error: "Escaping closures can only capture inout parameters explicitly by value".
The problem is inside this function:
fileprivate func collectAllAvailable(_ storage: inout [T], nextUrl: String, completion: #escaping CollectAllAvailableCompletion) {
if let client = self.client {
let _ : T? = client.collectionItems(nextUrl) {
(resultCollection, error) -> Void in
guard error == nil else {
completion(nil, error)
return
}
guard let resultCollection = resultCollection, let results = resultCollection.results else {
completion(nil, NSError.unhandledError(ResultCollection.self))
return
}
storage += results // Error: Escaping closures can only capture inout parameters explicitly by value
if let nextUrlItr = resultCollection.links?.url(self.nextResourse) {
self.collectAllAvailable(&storage, nextUrl: nextUrlItr, completion: completion)
// Error: Escaping closures can only capture inout parameters explicitly by value
} else {
completion(storage, nil)
// Error: Escaping closures can only capture inout parameters explicitly by value
}
}
} else {
completion(nil, NSError.unhandledError(ResultCollection.self))
}
}
Can someone help me to fix that?
Using an inout parameter exclusively for an asynchronous task is an abuse of inout – as when calling the function, the caller's value that is passed into the inout parameter will not be changed.
This is because inout isn't a pass-by-reference, it's just a mutable shadow copy of the parameter that's written back to the caller when the function exits – and because an asynchronous function exits immediately, no changes will be written back.
You can see this in the following Swift 2 example, where an inout parameter is allowed to be captured by an escaping closure:
func foo(inout val: String, completion: (String) -> Void) {
dispatch_async(dispatch_get_main_queue()) {
val += "foo"
completion(val)
}
}
var str = "bar"
foo(&str) {
print($0) // barfoo
print(str) // bar
}
print(str) // bar
Because the closure that is passed to dispatch_async escapes the lifetime of the function foo, any changes it makes to val aren't written back to the caller's str – the change is only observable from being passed into the completion function.
In Swift 3, inout parameters are no longer allowed to be captured by #escaping closures, which eliminates the confusion of expecting a pass-by-reference. Instead you have to capture the parameter by copying it, by adding it to the closure's capture list:
func foo(val: inout String, completion: #escaping (String) -> Void) {
DispatchQueue.main.async {[val] in // copies val
var val = val // mutable copy of val
val += "foo"
completion(val)
}
// mutate val here, otherwise there's no point in it being inout
}
(Edit: Since posting this answer, inout parameters can now be compiled as a pass-by-reference, which can be seen by looking at the SIL or IR emitted. However you are unable to treat them as such due to the fact that there's no guarantee whatsoever that the caller's value will remain valid after the function call.)
However, in your case there's simply no need for an inout. You just need to append the resultant array from your request to the current array of results that you pass to each request.
For example:
fileprivate func collectAllAvailable(_ storage: [T], nextUrl: String, completion: #escaping CollectAllAvailableCompletion) {
if let client = self.client {
let _ : T? = client.collectionItems(nextUrl) { (resultCollection, error) -> Void in
guard error == nil else {
completion(nil, error)
return
}
guard let resultCollection = resultCollection, let results = resultCollection.results else {
completion(nil, NSError.unhandledError(ResultCollection.self))
return
}
let storage = storage + results // copy storage, with results appended onto it.
if let nextUrlItr = resultCollection.links?.url(self.nextResourse) {
self.collectAllAvailable(storage, nextUrl: nextUrlItr, completion: completion)
} else {
completion(storage, nil)
}
}
} else {
completion(nil, NSError.unhandledError(ResultCollection.self))
}
}
If you want to modify a variable passed by reference in an escaping closure, you can use KeyPath. Here is an example:
class MyClass {
var num = 1
func asyncIncrement(_ keyPath: WritableKeyPath<MyClass, Int>) {
DispatchQueue.main.async {
// Use weak to avoid retain cycle
[weak self] in
self?[keyPath: keyPath] += 1
}
}
}
You can see the full example here.
If you are sure that your variable will be available the whole time just use a true Pointer (same what inout actually does)
func foo(val: UnsafeMutablePointer<NSDictionary>, completion: #escaping (NSDictionary) -> Void) {
val.pointee = NSDictionary()
DispatchQueue.main.async {
completion(val.pointee)
}
}

Cannot change the value of a global variable

I just wrote some Swift code for accessing Riot API, with Alamofire and SwiftyJSON.
I wrote a function func getIDbyName(SummName: String) -> String to get the summoner id.
As you can see from the code below, I am assigning the id to self.SummID.
After executing the function, I am able to println the correct id, for example "1234567". However, the return self.SummID returns "0", the same as assigned in the beginning.
I tried to mess with the code, but I simply cannot get the correct value of self.SummID outside of the Alamofire.request closure. It always remain "0" anywhere outside.
I think it has something to do with the scope of the variable. Does anyone know what is going on here?
import Foundation
import Alamofire
import SwiftyJSON
class SummInfo {
var SummName = "ThreeSmokingGuns"
var SummID = "0"
var SummChamp = "akali"
var SummS1 = "flash"
var SummS2 = "ignite"
var SummRank = "Unranked"
var SummWR = "-" //summoner's winrate
let api_key = "key"
let URLinsert = "?api_key="
init(SummName: String, SummChamp: String, SummS1: String, SummS2: String, SummRank: String, SummWR: String) {
self.SummName = SummName
self.SummChamp = SummChamp
self.SummS1 = SummS1
self.SummS2 = SummS2
self.SummRank = SummRank
self.SummWR = SummWR
}
init(SummName: String) {
self.SummName = SummName
}
func getIDbyName(SummName: String) -> String
{
let SummURL = "https://na.api.pvp.net/api/lol/na/v1.4/summoner/by-name/"
var fullURL = "\(SummURL)\(SummName)\(URLinsert)\(api_key)"
Alamofire.request(.GET, fullURL)
.responseJSON { (request, response, data, error) in
if let anError = error
{
// got an error in getting the data, need to handle it
println("error calling GET on /posts/1")
println(error)
}
else if let data: AnyObject = data // hate this but responseJSON gives us AnyObject? while JSON() expects AnyObject
// JSON(data!) will crash if we get back empty data, so we keep the one ugly unwrapping line
{
// handle the results as JSON, without a bunch of nested if loops
let post = JSON(data)
self.tempJ = post
var key = post.dictionaryValue.keys.array //not necessary
var key2 = post[SummName.lowercaseString].dictionaryValue.keys.array
self.SummID = post[key[0],key2[2]].stringValue //[profileIconId, revisionDate, id, summonerLevel, name]
//test console output
println("The post is: \(post.description)")
println(SummName.lowercaseString)
println(key)
println(key2)
println(self.SummID)
}
}
return self.SummID
}
}
The reason is that
Alamofire.request(.GET, fullURL)
.responseJSON
is an asynchronous call. This means that the call to getIDbyName will immediately return without waiting the responseJSON to finish. This is the exact reason why you get a the '0' value for ID that you have set initially.
Having said that, the solution is to have a call back closure in the getIDbyName method:
func getIDbyName(SummName: String, callback: (id:String?) ->() ) -> ()
{
let SummURL = "https://na.api.pvp.net/api/lol/na/v1.4/summoner/by-name/"
var fullURL = "\(SummURL)\(SummName)\(URLinsert)\(api_key)"
Alamofire.request(.GET, fullURL)
.responseJSON { (request, response, data, error) in
if let anError = error
{
// got an error in getting the data, need to handle it
println("error calling GET on /posts/1")
println(error)
//Call back closure with nil value
callback(nil) //Can additionally think of passing actual error also here
}
else if let data: AnyObject = data // hate this but responseJSON gives us AnyObject? while JSON() expects AnyObject
// JSON(data!) will crash if we get back empty data, so we keep the one ugly unwrapping line
{
// handle the results as JSON, without a bunch of nested if loops
let post = JSON(data)
self.tempJ = post
var key = post.dictionaryValue.keys.array //not necessary
var key2 = post[SummName.lowercaseString].dictionaryValue.keys.array
self.SummID = post[key[0],key2[2]].stringValue //[profileIconId, revisionDate, id, summonerLevel, name]
//test console output
println("The post is: \(post.description)")
println(SummName.lowercaseString)
println(key)
println(key2)
println(self.SummID)
//Pass the actual ID got.
callback(self.SummID)
}
}
return self.SummID
}
And clients should always use this API to fetch the latest ID, and can refer the attribute directly to get whatever is cached so far in SummID member.
Here is how to call this method-
object.getIDbyName(sumName){ (idString :String) in
//Do whatever with the idString
}

Resources