Function not returning value from closure - ios

I need to return a value from a function that has a closure in it.
I researched about returning value from closures, and found out that I should use 'completion handler' to get the result I want.
I saw posts here and articles explaining it but could not apply because I didn't find anything that matches with my problem.
class ViewController: UIViewController {
let urls = URLs()
override func viewDidLoad() {
super.viewDidLoad()
var leagueId = getLeagueId(country: "brazil", season: "2019")
print(leagueId) //PRINTING 0
}
func getLeagueId (country: String, season: String) -> Int {
let headers = Headers().getHeaders()
var leagueId = 0
let url = urls.getLeagueUrlByCountryAndSeason(country: country, season: season)
Alamofire.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers).responseJSON {
response in
if response.result.isSuccess {
let leagueJSON: JSON = JSON(response.result.value!)
leagueId = (leagueJSON["api"]["leagues"][0]["league_id"].intValue)
}
else {
print("error")
}
}
return leagueId
}
}
The value returned is always 0 because the closure value is not passing to the function itself.
Thanks a lot

So the reason why you're having this issue is because AlamoFire.request is asynchronous. There's a great explanation of asynchronous vs synchronous here But basically when you execute something asynchronously, the compiler does not wait for the task to complete before continuing to the next task, but instead will execute the next task immediately.
So in your case, the AlamoFire.request is executed, and while it's running, the next line after the block is immediately run which is the line to return leagueId which will obviously still be equal to zero since the AlamoFire.request task (function) has not yet finished.
This is why you need to use a closure. A closure will allow you to return the value, after AlamoFire.request (or any other asynchronous task for that matter) has finished running. Manav's answer above shows you the proper way to do this in Swift. I just thought I'd help you understand why this is necessary.
Hope this helps somewhat!
Edit:
Manav's answer above is actually partially correct. Here's how you make it so you can reuse that value the proper way.
var myLeagueId = 0;
getLeagueId(country: "brazil", season: "2019",success: { (leagueId) in
// leagueId is the value returned from the closure
myLeagueId = leagueId
print(myLeagueId)
})
The code below will not work because it's setting myLeagueId to the return value of getLeagueId and getLeagueId doesn't have a return value so it won't even compile.
myLeagueId = getLeagueId(country: "brazil", season: "2019",success: { (leagueId) in
print(leagueId)
})

You need to either return value from the function.
func getLeagueId (country: String, season: String)->Int
Else you need to use completion handlers.
func getLeagueId (country: String, season: String,success:#escaping (leagueId: Int) -> Void) {
let headers = Headers().getHeaders()
var leagueId = 0
let url = urls.getLeagueUrlByCountryAndSeason(country: country, season: season)
Alamofire.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers).responseJSON {
response in
if response.result.isSuccess {
let leagueJSON: JSON = JSON(response.result.value!)
leagueId = (leagueJSON["api"]["leagues"][0]["league_id"].intValue)
success(leagueId)
}
else {
print("error")
}
}
}
And then use it in your code :
getLeagueId(country: "brazil", season: "2019",success: { (leagueId) in
print(leagueId)
self.leagueId = leagueId
})

This is how you should implement completionBLock
func getLeagueId (country: String, season: String, completionBlock:((_ id: String, _ error: Error?) -> Void)?) {
let headers = Headers().getHeaders()
var leagueId = 0
let url = urls.getLeagueUrlByCountryAndSeason(country: country, season: season)
Alamofire.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers).responseJSON {
response in
if response.result.isSuccess {
let leagueJSON: JSON = JSON(response.result.value!)
if let leagueId = (leagueJSON["api"]["leagues"][0]["league_id"].intValue){
completionBlock?(leagueId,nil)
}else {
completionBlock?(nil,nil) // PASS YOUR ERROR
}
}
else {
completionBlock?(nil,nil) // PASS YOUR ERROR
}
}
}

func getLeagueId isn't return anything so you get 0, if you want to get the result from func getLeagueId you should add completion handler function that will update this value.

Related

Alamofire 5 Any In Dictionary

I am trying to make a post request with Alamofire 5. I have to use Dictionary<String, Any> for parameters. Because I am writing a wrapper for Alamofire. But it seems i can't be able to use Any object in a dictionary because Alamofire gives me a compiler error:
Value of protocol type 'Any' cannot conform to 'Encodable'; only struct/enum/class types can conform to protocols
What i've tried:
let encodedParameters = Dictionary<String, Any>
AF.request(url, method: .get, parameters: encodedParameters, headers: headers)
Some values will be string others will be integer in my dictionary. So i can't use constant type. How can i resolve this problem?
It's because you are using the newer method which requires the parameters parameter to be Encodable.Use the older Alamofire method and you will be fine:
AF.request(url, method: .get, parameters: encodedParameters, encoding: JSONEncoding.default, headers: headers)
Update: If you want to use the latest Alamofire 5 syntax create a struct and confirm it to encodable. Then create an object of the same struct with values and pass it.
To use the new request, you can create your own structs for your request parameters:
// you might have...
struct FooRequestParameters : Codable {
let paramName1: Int
let paramName2: String
}
// for another type of request, you might have different parameters...
struct BarRequestParameters: Codable {
let somethingElse: Bool
}
And you can pass a FooRequestParameters(paramName1: 1, paramName1: "hello") instead of your dictionary. This would be the same as passing the dictionary:
[
"paramName1": 1,
"paramName2": "hello"
]
The rationale behind this API change is likely to have more safety. With a [String: Any], you could very easily, say, give a String value to a parameter that is supposed to be Int, or type a parameter's name wrongly, or missed some parameters without realising... etc etc.
If you want pass parameters of [String: Any] you don't need to create structs and adopt the encodable protocol. This can be achieved like this:
import Foundation
import Alamofire
enum ServiceRouter {
case fetchCountries
case fetchUserRepositories
var baseURL: String {
switch self {
case .fetchCountries:
return "https://restcountries.eu/rest/v2"
}
}
var path: String {
switch self {
case .fetchCountries:
return "/all"
}
}
var method: HTTPMethod {
switch self {
case .fetchCountries:
return .get
default:
return .post
}
}
var parameters: Parameters? {
switch self {
case .fetchUserRepositories:
return ["per_page": 100]
default:
return [:]
}
}
}
// MARK: - URLRequestConvertible
extension ServiceRouter: URLRequestConvertible {
func asURLRequest() throws -> URLRequest {
let url = try baseURL.asURL().appendingPathComponent(path)
var request = URLRequest(url: url)
request.method = method
if method == .get {
request = try URLEncoding.default.encode(request, with: parameters)
} else if method == .post {
request = try JSONEncoding.default.encode(request, with: parameters)
}
return request
}
}
And you can use this request as
AF.request(ServiceRouter.fetchCountries).responseData { (response) in
With the Alamofire 5 you can use this with Any type
AF.request("{url}", method: .post, parameters: params, encoding: JSONEncoding.prettyPrinted).response { response in
print(response)
}
I hope this helps someone

Swift: How to save a Decodable.Protocol object to a variable?

In my application, several controllers have a very similar code structure, the differences are minimal, so for optimization I decided to create a basis for these controllers, and inherit each specific controller from the basis.
I have a function for sending network requests and processing a response, I pass the response structure as a parameter to this function, so that the function returns a ready-made response structure to me. Each such structure is Decodable.
An example of such a structure:
struct APIAnswerUserActivity: Decodable {
let status: String
let code: Int?
let data: [UserActivity]?
let total: Int?
}
Function for network requests, an object (structure) of the Decodable.Protocol type is accepted as a jsonType parameter:
public func networkRequest<T: Decodable> (
url: String,
timeout: Double = 30,
method: URLMethods = .GET,
data: [String : String]? = nil,
files: [URL]? = nil,
jsonType: T.Type,
success: #escaping (T) -> Void,
failure: #escaping (APIError) -> Void
) -> URLSessionDataTask { ... }
There are several parameters in the main controller that I override through override in the child controllers, one of these parameters should be an object of type Decodable for the general function to receive data correctly. The JSON structures of the response are very similar, but still slightly different, a common structure for them cannot be created, because the data is still a little different.
If in the main controller do this:
public var decodableType: Decodable.Type {
return APIAnswerUserActivity.self
}
That will work, and it is possible to redefine types, but the network function does not accept this, it needs the Decodable.Protocol object. If the type decodable.Protocol is specified for the variable decodableType, then it is no longer possible to add APIAnswerUserActivity.self, which is quietly accepted when the networkRequest function is called.
How to be in this situation? I hope that I managed to correctly and clearly state the essence of my problem. Thanks!
#Владислав Артемьев, I'm still not sure that I completely understand the problem because you haven't shared the code that takes the Decodable class. But the issues seems to be about how to pass a Decodable class.
I hope the following can help clarify how you can impose the right constraint on the generic and how you should declare the variable. You can paste it into a playground and experiment.
import Foundation
struct FakeToDo: Decodable {
var userId: Int
var id: Int
var title: String
var completed: Bool
}
enum URLMethods {
case GET
case POST
}
func networkRequest<T: Decodable> (
url: String,
timeout: Double = 30,
method: URLMethods = .GET,
data: [String : String]? = nil,
files: [URL]? = nil,
jsonType: T.Type,
success: #escaping (T) -> Void,
failure: #escaping (Error) -> Void
) -> URLSessionDataTask {
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { (data, response, error) in
if error != nil {
failure(error!)
return
}
guard let data = data else { return }
let decoder = JSONDecoder()
guard let value = try? decoder.decode(T.self, from: data) else { return }
// get back on the main queue for UI
DispatchQueue.main.async {
success(value)
}
})
return task
}
class Example<T> where T: Decodable {
let type: T.Type
init(_ type: T.Type) {
self.type = type
}
public var decodableType: T.Type {
return type
}
}
let decodableType = Example(FakeToDo.self).decodableType
let url = "https://jsonplaceholder.typicode.com/todos/1"
let task = networkRequest(url: url, jsonType: decodableType,
success: { value in print(value) },
failure: { error in print(error) })
task.resume()

Issue while calling mutlile API in swift 3?

I have two classes.Class A and Class B. In class A there is table view.When i tap on cell in class A i call first api to save the data /saveData & on response of first api i call another api getData.I call these API in background.Now when i move to class A i call the another API on viewDidLoad().I call this in foreground .Now i want that the API of class A should not effect the class B.
Please tell what is the best way to do that.
i tried DispatchGroup but did not work for me.
func saveInBackground(parameter : [String : AnyObject]?) -> Void {
let group = DispatchGroup()
group.notify(queue: DispatchQueue.global(qos: .background)){
let apiManager = APIHandler(baseURL: Constants.API.baseURL, APIVersion: "")
apiManager.requestOfBgMethod(.post, path: Constants.API.Name.addGeneralField.completePath, parameters: parameter, encoding: .url, headers: nil, apiSuccess: { (result) in
//update user
self.copyUser = User(copyFrom: self.user)
self.saveCVResponse(result: result)
//fetch data in background
Utility.sharedInstance.updateCVdata(cvManager: self.cvManager)
}, apiFailure: { (error) in
})
}
// DispatchQueue.global(qos: DispatchQoS.QoSClass.background).async {
//
//
// }
}
Use DispatchSemaphore and a Singleton instance to make API calling one by one:
class ApiHelper {
static let shardInstance = ApiHelper()
private let semaphore = DispatchSemaphore(value: 1) //one task allowed at a time
private let apiManager = APIHandler(baseURL: Constants.API.baseURL, APIVersion: "")
func saveInBackground(parameter : [String : AnyObject]?) -> Void {
DispatchQueue.global(qos: .default).async { [unowned self] in
self.semaphore.wait() //lock
apiManager.requestOfBgMethod(.post, path: Constants.API.Name.addGeneralField.completePath, parameters: parameter, encoding: .url, headers: nil, apiSuccess: { (result) in
//.......
self.semaphore.signal() //unlock
}, apiFailure: { (error) in
self.semaphore.signal() //unlock
})
}
}
}
Use it in Class A and Class B, they will not be effected each other:
ApiHelper.shardInstance.saveInBackground(parameter: nil)
Use global DispatchSemaphore without Singleton:
In class A or class B or somewhere, define a global semaphore out of class:
let semaphoreGlobal = DispatchSemaphore(value: 1) //one task at a time
//ignore following, just an example.
class AnyClass {
//......
}
And your method become:
func saveInBackground(parameter : [String : AnyObject]?) -> Void {
DispatchQueue.global(qos: .default).async {
semaphoreGlobal.wait() //wait if semaphore is 0. Else, set semaphore to 0 and continue.
//remember: the variable 'apiManager' should not be a local one.
apiManager.requestOfBgMethod(.post, path: Constants.API.Name.addGeneralField.completePath, parameters: parameter, encoding: .url, headers: nil, apiSuccess: { (result) in
//.......
semaphoreGlobal.signal() //set semaphore to 1, means allowing one of other tasks stops waiting.
}, apiFailure: { (error) in
semaphoreGlobal.signal() //set semaphore to 1
})
}
}

DispatchGroup.wait() freezes program

I am making two asynchronous network calls and would like to use a Dispatch Group to wait until the call complete and then resume. My program is freezing.
class CommentRatingViewController: UIViewController, UITextViewDelegate {
let myDispatchGroup = DispatchGroup()
#IBAction func saveRatingComment(_ sender: Any) {
rating = ratingView.rating
if rating != 0.0 {
myDispatchGroup.enter()
saveRating(articleID: post.articleID, userID: post.userID) //Network call
self.updatedRating = true
}
if commentsTextView.text != "" {
myDispatchGroup.enter()
saveComment(articleID: post.articleID, userID: post.userID, comment: commentsTextView.text!) //Network call self.addedComment = true
}
myDispatchGroup.wait()
DispatchQueue.main.async {
self.delegate?.didCommentOrRatePost(updatedRating: self.updatedRating, addedComment: self.addedComment)
}
}
And here is one of the network calls:
func saveRating (articleID: String, userID: String) {
let userPostURLRaw = "http://www.smarttapp.com/DesktopModules/DnnSharp/DnnApiEndpoint/Api.ashx?method=UpdatePostRating"
Alamofire.request(
userPostURLRaw,
method: .post,
parameters: ["articleID": articleID,
"newRating": self.rating,
"UserID": userID]
)
.responseString { response in
guard let myString = response.result.value else { return }
DispatchQueue.main.async {
self.myDispatchGroup.leave()
}
}
}
The network calls worked until I introduced Dispatch Group code.
I've resolved this.
The problem was that myDispatchGroup.enter() and self.myDispatchGroup.leave() where being called on different threads. I moved the call to the beginning and very end of the network requests and it works fine now.

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