Returning data from async call in Swift function
Asked Answered
S

13

113

I have created a utility class in my Swift project that handles all the REST requests and responses. I have built a simple REST API so I can test my code. I have created a class method that needs to return an NSArray but because the API call is async I need to return from the method inside the async call. The problem is the async returns void. If I were doing this in Node I would use JS promises but I can't figure out a solution that works in Swift.

import Foundation

class Bookshop {
    class func getGenres() -> NSArray {
        println("Hello inside getGenres")
        let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
        println(urlPath)
        let url: NSURL = NSURL(string: urlPath)
        let session = NSURLSession.sharedSession()
        var resultsArray:NSArray!
        let task = session.dataTaskWithURL(url, completionHandler: {data, response, error -> Void in
            println("Task completed")
            if(error) {
                println(error.localizedDescription)
            }
            var err: NSError?
            var options:NSJSONReadingOptions = NSJSONReadingOptions.MutableContainers
            var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: options, error: &err) as NSDictionary
            if(err != nil) {
                println("JSON Error \(err!.localizedDescription)")
            }
            //NSLog("jsonResults %@", jsonResult)
            let results: NSArray = jsonResult["genres"] as NSArray
            NSLog("jsonResults %@", results)
            resultsArray = results
            return resultsArray // error [anyObject] is not a subType of 'Void'
        })
        task.resume()
        //return "Hello World!"
        // I want to return the NSArray...
    }
}
Shahjahanpur answered 8/8, 2014 at 12:28 Comment(0)
F
104

You can pass callback, and call callback inside async call

something like:

class func getGenres(completionHandler: (genres: NSArray) -> ()) {
    ...
    let task = session.dataTaskWithURL(url) {
        data, response, error in
        ...
        resultsArray = results
        completionHandler(genres: resultsArray)
    }
    ...
    task.resume()
}

and then call this method:

override func viewDidLoad() {
    Bookshop.getGenres {
        genres in
        println("View Controller: \(genres)")     
    }
}
Fuentes answered 8/8, 2014 at 12:35 Comment(1)
Thanks for that. My final question is how do I call this class method from my view controller. The code is currently like this:override func viewDidLoad() { super.viewDidLoad() var genres = Bookshop.getGenres() // Missing argument for parameter #1 in call //var genres:NSArray //Bookshop.getGenres(genres) NSLog("View Controller: %@", genres) }Shahjahanpur
M
27

Introduced in Swift 5.5 (iOS 15, macOS 12), we would now use the async-await pattern:

func fetchGenres() async throws -> [Genre] {
    …
    let (data, _) = try await URLSession.shared.dataTask(for: request)
    return try JSONDecoder().decode([Genre].self, from: data)
}

And we would call it like:

let genres = try await fetchGenres()

The async-await syntax is far more concise and natural than the traditional completion handler pattern outlined in my original answer, below.

For more information, see Meet async/await in Swift.


The historic pattern is to use completion handlers closure.

For example, we would often use Result:

func fetchGenres(completion: @escaping (Result<[Genre], Error>) -> Void) {
    ...
    URLSession.shared.dataTask(with: request) { data, _, error in 
        if let error = error {
            DispatchQueue.main.async {
                completion(.failure(error))
            }
            return
        }

        // parse response here

        let results = ...
        DispatchQueue.main.async {
            completion(.success(results))
        }
    }.resume()
}

And you’d call it like so:

fetchGenres { results in
    switch results {
    case .failure(let error):
        print(error.localizedDescription)

    case .success(let genres):
        // use `genres` here, e.g. update model and UI            
    }
}

// but don’t try to use `genres` here, as the above runs asynchronously

Note, above I’m dispatching the completion handler back to the main queue to simplify model and UI updates. Some developers take exception to this practice and either use whatever queue URLSession used or use their own queue (requiring the caller to manually synchronize the results themselves).

But that’s not material here. The key issue is the use of completion handler to specify the block of code to be run when the asynchronous request is done.


Note, above I retired the use of NSArray (we don’t use those bridged Objective-C types any more). I assume that we had a Genre type and we presumably used JSONDecoder, rather than JSONSerialization, to decode it. But this question didn’t have enough information about the underlying JSON to get into the details here, so I omitted that to avoid clouding the core issue, the use of closures as completion handlers.

Mellisamellisent answered 8/2, 2019 at 17:10 Comment(2)
You can use Result in Swift 4 and lower, too, but you have to declare the enum yourself. I’m using this kind of pattern for years.Darnell
Yes, of course, as have I. But it only looks like it’s been embraced by Apple with the release of Swift 5. They’re just late to the party.Mellisamellisent
P
15

Swiftz already offers Future, which is the basic building block of a Promise. A Future is a Promise that cannot fail (all terms here are based on the Scala interpretation, where a Promise is a Monad).

https://github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift

Hopefully will expand to a full Scala-style Promise eventually (I may write it myself at some point; I'm sure other PRs would be welcome; it's not that difficult with Future already in place).

In your particular case, I would probably create a Result<[Book]> (based on Alexandros Salazar's version of Result). Then your method signature would be:

class func fetchGenres() -> Future<Result<[Book]>> {

Notes

  • I do not recommend prefixing functions with get in Swift. It will break certain kinds of interoperability with ObjC.
  • I recommend parsing all the way down to a Book object before returning your results as a Future. There are several ways this system can fail, and it's much more convenient if you check for all of those things before wrapping them up into a Future. Getting to [Book] is much better for the rest of your Swift code than handing around an NSArray.
Pretense answered 8/8, 2014 at 13:1 Comment(5)
Swiftz no longer support Future. But take a look at github.com/mxcl/PromiseKit it works great with Swiftz!Achernar
took me a few seconds to realize you didn't write Swift and wrote SwiftzColiseum
It sounds like "Swiftz" is a third party functional library for Swift. Since your answer seems to be based on that library, you should state that explicitly. (e.g. "There is a third party library called 'Swiftz' that supports functional constructs like Futures, and should serve as a good starting-point if you want to implement Promises.") Otherwise your readers are just going to wonder why you misspelled "Swift".Bitt
Please note that github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift is not working anymore.Execution
@Mellisamellisent The get prefix indicates return-by-reference in ObjC (such as in -[UIColor getRed:green:blue:alpha:]). When I wrote this I was concerned that the importers would leverage that fact (to return a tuple automatically for example). It's turned out that they haven't. When I wrote this I probably had also forgotten that KVC supports "get" prefixes for accessors (it's something I've learned and forgotten several times). So agreed; I haven't run into any cases where the leading get breaks things. It's just misleading to those who know the meaning of ObjC "get."Pretense
P
9

Swift 4.0

For async Request-Response you can use completion handler. See below I have modified the solution with completion handle paradigm.

func getGenres(_ completion: @escaping (NSArray) -> ()) {

        let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
        print(urlPath)

        guard let url = URL(string: urlPath) else { return }

        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard let data = data else { return }
            do {
                if let jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary {
                    let results = jsonResult["genres"] as! NSArray
                    print(results)
                    completion(results)
                }
            } catch {
                //Catch Error here...
            }
        }
        task.resume()
    }

You can call this function as below:

getGenres { (array) in
    // Do operation with array
}
Parachute answered 13/6, 2018 at 4:22 Comment(0)
I
4

Swift 3 version of @Alexey Globchastyy's answer:

class func getGenres(completionHandler: @escaping (genres: NSArray) -> ()) {
...
let task = session.dataTask(with:url) {
    data, response, error in
    ...
    resultsArray = results
    completionHandler(genres: resultsArray)
}
...
task.resume()
}
Impotence answered 16/5, 2017 at 18:55 Comment(0)
T
4

Swift 5.5, async/wait-based solution

The original test URL provided by the original poster is no longer functional, so I had to change things a bit. This solution is based on a jokes API I found. That API returns a single joke, but I return it as an array of String ([String]), to keep it as consistent as possible with the original post.


class Bookshop {
    class func getGenres() async -> [String] {
        print("Hello inside getGenres")
        let urlPath = "https://geek-jokes.sameerkumar.website/api?format=json"
        print(urlPath)
        let url = URL(string: urlPath)!
        let session = URLSession.shared
        
        typealias Continuation = CheckedContinuation<[String], Never>
        let genres = await withCheckedContinuation { (continuation: Continuation) in
            let task = session.dataTask(with: url) { data, response, error in
                print("Task completed")
                
                var result: [String] = []
                defer {
                    continuation.resume(returning: result)
                }
                
                if let error = error {
                    print(error.localizedDescription)
                    return
                }
                guard let data = data else { 
                    return
                }
                
                do {
                    let jsonResult = try JSONSerialization.jsonObject(with: data, options: [.mutableContainers])
                    print("jsonResult is \(jsonResult)")
                    if let joke = (jsonResult as? [String: String])?["joke"] {
                        result = [joke]
                    }
                } catch {
                    print("JSON Error \(error.localizedDescription)")
                    print("data was \(String(describing: String(data: data, encoding: .utf8)))")
                    return
                }
            }
            task.resume()
        }
        
        return genres
    }
}

async {
    let final = await Bookshop.getGenres()
    print("Final is \(final)")
}

The withCheckedContinuation is how you made the Swift async function actually run in a separate task/thread.

Torosian answered 10/6, 2021 at 18:5 Comment(1)
I had a post about a very similar issue, and it was annoyingly closed down and referred to this post. Lots of great stuff here, but doesn't help me in the least unfortunatly. #76958624 That is the post, I am trying to go into an array of google places, run functions that get the coordinates, and then put that into a struct from a compactMap. I have tried to follow all examples here, but I cannot get this to work. I know I am missing something hereMusca
T
3

I hope you're not still stuck on this, but the short answer is that you can't do this in Swift.

An alternative approach would be to return a callback that will provide the data you need as soon as it is ready.

Twiggy answered 15/11, 2018 at 17:35 Comment(2)
He can do promises in swift too. But apple's current recommended aproceh is using callback with closures as you point out or to use delegation like the older cocoa API'sZamir
You right about Promises. But Swift does not provide a native API for this, so he has to use PromiseKit or other alternative.Twiggy
F
3

There are 3 ways of creating call back functions namely: 1. Completion handler 2. Notification 3. Delegates

Completion Handler Inside set of block is executed and returned when source is available, Handler will wait until response comes so that UI can be updated after.

Notification Bunch of information is triggered over all the app, Listner can retrieve n make use of that info. Async way of getting info through out the project.

Delegates Set of methods will get triggered when delegate is been called, Source must be provided via methods itself

Frustum answered 21/2, 2019 at 11:2 Comment(0)
W
2

Swift 5.5:

TL;DR: Swift 5.5 is not yet released(at the time of writing). To use swift 5.5, download swift toolchain development snapshot from here and add compiler flag -Xfrontend -enable-experimental-concurrency. Read more here

This can be achieved easily with async/await feature.

To do so, you should mark your function as async then do the operation inside withUnsafeThrowingContinuation block like following.

class Bookshop {
  class func getGenres() async throws -> NSArray {
    print("Hello inside getGenres")
    let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
    print(urlPath)
    let url = URL(string: urlPath)!
    let session = URLSession.shared
    return try await withUnsafeThrowingContinuation { continuation in
      let task = session.dataTask(with: url, completionHandler: {data, response, error -> Void in
        print("Task completed")
        if(error != nil) {
          print(error!.localizedDescription)
          continuation.resume(throwing: error!)
          return
        }
        do {
          let jsonResult = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String: Any]
          let results: NSArray = jsonResult!["genres"] as! NSArray
          continuation.resume(returning: results)
        } catch {
          continuation.resume(throwing: error)
        }
      })
      task.resume()
    }
  }
}

And you can call this function like

@asyncHandler
func check() {
  do {
    let genres = try await Bookshop.getGenres()
    print("Result: \(genres)")
  } catch {
    print("Error: \(error)")
  }
}

Keep in mind that, when calling Bookshop.getGenres method, the caller method should be either async or marked as @asyncHandler

Wergild answered 30/4, 2021 at 11:29 Comment(0)
T
-1
self.urlSession.dataTask(with: request, completionHandler: { (data, response, error) in
            self.endNetworkActivity()

            var responseError: Error? = error
            // handle http response status
            if let httpResponse = response as? HTTPURLResponse {

                if httpResponse.statusCode > 299 , httpResponse.statusCode != 422  {
                    responseError = NSError.errorForHTTPStatus(httpResponse.statusCode)
                }
            }

            var apiResponse: Response
            if let _ = responseError {
                apiResponse = Response(request, response as? HTTPURLResponse, responseError!)
                self.logError(apiResponse.error!, request: request)

                // Handle if access token is invalid
                if let nsError: NSError = responseError as NSError? , nsError.code == 401 {
                    DispatchQueue.main.async {
                        apiResponse = Response(request, response as? HTTPURLResponse, data!)
                        let message = apiResponse.message()
                        // Unautorized access
                        // User logout
                        return
                    }
                }
                else if let nsError: NSError = responseError as NSError? , nsError.code == 503 {
                    DispatchQueue.main.async {
                        apiResponse = Response(request, response as? HTTPURLResponse, data!)
                        let message = apiResponse.message()
                        // Down time
                        // Server is currently down due to some maintenance
                        return
                    }
                }

            } else {
                apiResponse = Response(request, response as? HTTPURLResponse, data!)
                self.logResponse(data!, forRequest: request)
            }

            self.removeRequestedURL(request.url!)

            DispatchQueue.main.async(execute: { () -> Void in
                completionHandler(apiResponse)
            })
        }).resume()
Thinkable answered 19/5, 2018 at 16:42 Comment(0)
W
-1

There are mainly 3 ways of achieving callback in swift

  1. Closures/Completion handler

  2. Delegates

  3. Notifications

Observers can also be used to get notified once the async task has been completed.

Woodcut answered 14/2, 2019 at 10:23 Comment(0)
E
-1

There are some very generic requirements that would like every good API Manager to satisfy: will implement a protocol-oriented API Client.

APIClient Initial Interface

protocol APIClient {
   func send(_ request: APIRequest,
              completion: @escaping (APIResponse?, Error?) -> Void) 
}

protocol APIRequest: Encodable {
    var resourceName: String { get }
}

protocol APIResponse: Decodable {
}

Now Please check complete api structure

// ******* This is API Call Class  *****
public typealias ResultCallback<Value> = (Result<Value, Error>) -> Void

/// Implementation of a generic-based  API client
public class APIClient {
    private let baseEndpointUrl = URL(string: "irl")!
    private let session = URLSession(configuration: .default)

    public init() {

    }

    /// Sends a request to servers, calling the completion method when finished
    public func send<T: APIRequest>(_ request: T, completion: @escaping ResultCallback<DataContainer<T.Response>>) {
        let endpoint = self.endpoint(for: request)

        let task = session.dataTask(with: URLRequest(url: endpoint)) { data, response, error in
            if let data = data {
                do {
                    // Decode the top level response, and look up the decoded response to see
                    // if it's a success or a failure
                    let apiResponse = try JSONDecoder().decode(APIResponse<T.Response>.self, from: data)

                    if let dataContainer = apiResponse.data {
                        completion(.success(dataContainer))
                    } else if let message = apiResponse.message {
                        completion(.failure(APIError.server(message: message)))
                    } else {
                        completion(.failure(APIError.decoding))
                    }
                } catch {
                    completion(.failure(error))
                }
            } else if let error = error {
                completion(.failure(error))
            }
        }
        task.resume()
    }

    /// Encodes a URL based on the given request
    /// Everything needed for a public request to api servers is encoded directly in this URL
    private func endpoint<T: APIRequest>(for request: T) -> URL {
        guard let baseUrl = URL(string: request.resourceName, relativeTo: baseEndpointUrl) else {
            fatalError("Bad resourceName: \(request.resourceName)")
        }

        var components = URLComponents(url: baseUrl, resolvingAgainstBaseURL: true)!

        // Common query items needed for all api requests
        let timestamp = "\(Date().timeIntervalSince1970)"
        let hash = "\(timestamp)"
        let commonQueryItems = [
            URLQueryItem(name: "ts", value: timestamp),
            URLQueryItem(name: "hash", value: hash),
            URLQueryItem(name: "apikey", value: "")
        ]

        // Custom query items needed for this specific request
        let customQueryItems: [URLQueryItem]

        do {
            customQueryItems = try URLQueryItemEncoder.encode(request)
        } catch {
            fatalError("Wrong parameters: \(error)")
        }

        components.queryItems = commonQueryItems + customQueryItems

        // Construct the final URL with all the previous data
        return components.url!
    }
}

// ******  API Request Encodable Protocol *****
public protocol APIRequest: Encodable {
    /// Response (will be wrapped with a DataContainer)
    associatedtype Response: Decodable

    /// Endpoint for this request (the last part of the URL)
    var resourceName: String { get }
}

// ****** This Results type  Data Container Struct ******
public struct DataContainer<Results: Decodable>: Decodable {
    public let offset: Int
    public let limit: Int
    public let total: Int
    public let count: Int
    public let results: Results
}
// ***** API Errro Enum ****
public enum APIError: Error {
    case encoding
    case decoding
    case server(message: String)
}


// ****** API Response Struct ******
public struct APIResponse<Response: Decodable>: Decodable {
    /// Whether it was ok or not
    public let status: String?
    /// Message that usually gives more information about some error
    public let message: String?
    /// Requested data
    public let data: DataContainer<Response>?
}

// ***** URL Query Encoder OR JSON Encoder *****
enum URLQueryItemEncoder {
    static func encode<T: Encodable>(_ encodable: T) throws -> [URLQueryItem] {
        let parametersData = try JSONEncoder().encode(encodable)
        let parameters = try JSONDecoder().decode([String: HTTPParam].self, from: parametersData)
        return parameters.map { URLQueryItem(name: $0, value: $1.description) }
    }
}

// ****** HTTP Pamater Conversion Enum *****
enum HTTPParam: CustomStringConvertible, Decodable {
    case string(String)
    case bool(Bool)
    case int(Int)
    case double(Double)

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()

        if let string = try? container.decode(String.self) {
            self = .string(string)
        } else if let bool = try? container.decode(Bool.self) {
            self = .bool(bool)
        } else if let int = try? container.decode(Int.self) {
            self = .int(int)
        } else if let double = try? container.decode(Double.self) {
            self = .double(double)
        } else {
            throw APIError.decoding
        }
    }

    var description: String {
        switch self {
        case .string(let string):
            return string
        case .bool(let bool):
            return String(describing: bool)
        case .int(let int):
            return String(describing: int)
        case .double(let double):
            return String(describing: double)
        }
    }
}

/// **** This is your API Request Endpoint  Method in Struct *****
public struct GetCharacters: APIRequest {
    public typealias Response = [MyCharacter]

    public var resourceName: String {
        return "characters"
    }

    // Parameters
    public let name: String?
    public let nameStartsWith: String?
    public let limit: Int?
    public let offset: Int?

    // Note that nil parameters will not be used
    public init(name: String? = nil,
                nameStartsWith: String? = nil,
                limit: Int? = nil,
                offset: Int? = nil) {
        self.name = name
        self.nameStartsWith = nameStartsWith
        self.limit = limit
        self.offset = offset
    }
}

// *** This is Model for Above Api endpoint method ****
public struct MyCharacter: Decodable {
    public let id: Int
    public let name: String?
    public let description: String?
}


// ***** These below line you used to call any api call in your controller or view model ****
func viewDidLoad() {
    let apiClient = APIClient()

    // A simple request with no parameters
    apiClient.send(GetCharacters()) { response in

        response.map { dataContainer in
            print(dataContainer.results)
        }
    }

}
Edh answered 17/4, 2019 at 9:33 Comment(0)
R
-1

This is a small use case that might be helpful:-

func testUrlSession(urlStr:String, completionHandler: @escaping ((String) -> Void)) {
        let url = URL(string: urlStr)!


        let task = URLSession.shared.dataTask(with: url){(data, response, error) in
            guard let data = data else { return }
            if let strContent = String(data: data, encoding: .utf8) {
            completionHandler(strContent)
            }
        }


        task.resume()
    }

While calling the function:-

testUrlSession(urlStr: "YOUR-URL") { (value) in
            print("Your string value ::- \(value)")
}
Roscoe answered 23/4, 2019 at 17:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.