By Mark Tyers


2014-08-08 12:28:59 8 Comments

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...
    }
}

10 comments

@TalBenAsulo 2019-02-25 15:09:21

Use completion blocks and activate then on the main thread.

The main thread is the UI thread, whenever you make an async task and you want to update the UI you must do all the UI changes on the UI thread

example:

    func asycFunc(completion: () -> Void) {

                URLSession.shared.dataTask(with: request) { data, _, error in 
                    // This is an async task...!!
                    if let error = error {
                    }

                  DispatchQueue.main.async(execute: { () -> Void in
                  //When the async taks will be finished this part of code will run on the main thread
                  completion()
                })

        }

}

@IRANNA SALUNKE 2019-02-21 11:02:07

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

@HSAM 2019-02-14 10:23:10

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.

@Rob 2019-02-08 17:10:41

The basic pattern is to use completion handlers closure.

For example, in the forthcoming Swift 5, you’d 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 .success(let genres):
        // use genres here, e.g. update model and UI

    case .failure(let error):
        print(error.localizedDescription)
    }
}

// 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.


The older, Swift 4 pattern is:

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

        // parse response here

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

And you’d call it like so:

fetchGenres { genres, error in
    guard let genres = genres, error == nil else {
        // handle failure to get valid response here

        return
    }

    // use genres here
}

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

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.

@vadian 2019-02-14 10:40:27

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.

@Rob 2019-02-14 16:50:05

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.

@LironXYZ 2018-11-15 17:35:39

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.

@Mojtaba Hosseini 2018-11-16 00:00:08

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's

@LironXYZ 2018-11-16 05:37:09

You right about Promises. But Swift does not provide a native API for this, so he has to use PromiseKit or other alternative.

@Jaydeep 2018-06-13 04:22:31

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
}

@Tum 2018-09-28 15:07:36

Sweet, works great. Thanks!

@Jaydeep 2019-03-04 12:08:46

To Down voters - Can you share me reason for downvote. So i can refine the solution.

@CrazyPro007 2018-05-19 16:42:14

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()

@Nebojsa Nadj 2017-05-16 18:55:32

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()
}

@Rob Napier 2014-08-08 13:01:02

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.

@badeleux 2015-07-28 08:45:41

Swiftz no longer support Future. But take a look at github.com/mxcl/PromiseKit it works great with Swiftz!

@Honey 2017-08-18 19:01:56

took me a few seconds to realize you didn't write Swift and wrote Swiftz

@Duncan C 2018-01-02 21:55:33

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".

@Ahmad F 2018-03-01 07:16:17

Please note that github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift is not working anymore.

@Rob 2019-02-08 16:27:23

"I do not recommend prefixing functions with get in Swift. It will break certain kinds of interoperability with ObjC.” ... Hmm, what interoperability will it break? Objective-C getters don’t use get prefix, so I’m not sure what you’re suggesting will break. Don’t get me wrong, I’d avoid use of get prefix simply to avoid confusion that so many languages do use get as prefix for getters, but AFAIK, there’s no real risk of anything actually breaking because of this prefix.

@Rob Napier 2019-02-08 17:33:11

@Rob 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."

@Alexey Globchastyy 2014-08-08 12:35:08

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)")     
    }
}

@Mark Tyers 2014-08-08 12:56:26

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) }

@Alexey Globchastyy 2014-08-08 13:00:11

added it to my answer

@The_Curry_Man 2016-06-16 00:34:09

Thank you so much! You won't believe how long I spent on this issue before I found your answer!!

Related Questions

Sponsored Content

33 Answered Questions

[SOLVED] How do I return the response from an asynchronous call?

15 Answered Questions

[SOLVED] How to call Objective-C code from Swift

  • 2014-06-02 20:05:42
  • David Mulder
  • 257787 View
  • 884 Score
  • 15 Answer
  • Tags:   objective-c swift

17 Answered Questions

6 Answered Questions

[SOLVED] async/await - when to return a Task vs void?

3 Answered Questions

I keep getting a use unresolved identifier error swift

3 Answered Questions

2 Answered Questions

Action extension loadItemForTypeIdentifier returns UIImage instead of NSURL

1 Answered Questions

[SOLVED] Swift downloading data from web

  • 2015-03-24 09:35:52
  • Stevik
  • 834 View
  • 5 Score
  • 1 Answer
  • Tags:   swift

1 Answered Questions

[SOLVED] how to use JsonArray out of Queue

1 Answered Questions

[SOLVED] FaceBook Connect. Login on tap

Sponsored Content