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

6 comments

@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!

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

@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

17 Answered Questions

15 Answered Questions

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

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

33 Answered Questions

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

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
  • 814 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