63

I am making url calls thru an API that I created using swift as follows:

class API {

  let apiEndPoint = "endpoint"
  let apiUrl:String!
  let consumerKey:String!
  let consumerSecret:String!

  var returnData = [:]

  init(){
    self.apiUrl = "https://myurl.com/"
    self.consumerKey = "my consumer key"
    self.consumerSecret = "my consumer secret"
  }

  func getOrders() -> NSDictionary{
    return makeCall("orders")
  }

  func makeCall(section:String) -> NSDictionary{

    let params = ["consumer_key":"key", "consumer_secret":"secret"]

    Alamofire.request(.GET, "\(self.apiUrl)/\(self.apiEndPoint + section)", parameters: params)
        .authenticate(user: self.consumerKey, password: self.consumerSecret)
        .responseJSON { (request, response, data, error) -> Void in
            println("error \(request)")
            self.returnData = data! as NSDictionary
    }
    return self.returnData
  }

}

I call this API in my UITableViewController to populate the table with SwiftyJSON library. However my returnData from the API is always empty. There is no problem with Alomofire calls as I can successfully retrieve value. My problem is how I am supposed to carry this data over to my table view controller?

var api = API()
api.getOrders()
println(api.returnData) // returnData is empty
Rob
  • 415,655
  • 72
  • 787
  • 1,044
u54r
  • 1,767
  • 2
  • 15
  • 26
  • Unrelated to your original question, I'm not at all confident of how you're using the consumer key and the secret (you're using the Alamofire `authenticate` process, but doing something curious with the parameters, too). It's probably one or the other. It's a function of your web service's API, so we cannot answer that here, but this is almost certainly not correct. – Rob Dec 10 '14 at 04:06
  • It's how the service API I use is designed. Documentation suggests passing key and secret as username and password for basic authentication. – u54r Dec 10 '14 at 05:49
  • OK, if it really uses BASIC authentication, then use the `authenticate` function, but then what is the purpose the `params` dictionary? Hey, whatever works, but it seems curious to do both `authenticate` and pass it auth details again as parameters to the query... – Rob Dec 10 '14 at 11:48

5 Answers5

105

As mattt points out, Alamofire is returning data asynchronously via a “completion handler” pattern, so you must do the same. You cannot just return the value immediately, but you instead want to change your method to not return anything, but instead use a completion handler closure pattern.

Nowadays, that might look like:

func getOrders(completionHandler: @escaping (Result<[String: Any]>) -> Void) {
    performRequest("orders", completion: completionHandler)
}

func performRequest(_ section: String, completion: @escaping (Result<[String: Any]>) -> Void) {
    let url = baseURL.appendingPathComponent(section)
    let params = ["consumer_key": "key", "consumer_secret": "secret"]

    Alamofire.request(url, parameters: params)
        .authenticate(user: consumerKey, password: consumerSecret)
        .responseJSON { response in
            switch response.result {
            case .success(let value as [String: Any]):
                completion(.success(value))

            case .failure(let error):
                completion(.failure(error))

            default:
                fatalError("received non-dictionary JSON response")
            }
    }
}

Then, when you want to call it, you use this completion closure parameter (in trailing closure, if you want):

api.getOrders { result in
    switch result {
    case .failure(let error):
        print(error)

    case .success(let value):
        // use `value` here
    }
}

// but don't try to use the `error` or `value`, as the above closure
// has not yet been called
//
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Hi @Rob, I have used your code almost one-for-one to make an asynchronous request. My goal as a start is to get a [String] and use that variable.count to determine how many numberOfRowsInSection in my tableView. Your example really helped me, but there's nothing in it that actually "returns" something. When I do `ConnectionsHelper.getOrders()` in my own code, I can print details about my `responseObject` successfully, but not in any way return them or assign them to a variable in my ViewController file. Can you give a few more pointers? Thanks! – zerohedge Dec 21 '15 at 16:43
  • 3
    Correct, this doesn't "return" anything. It can't because it runs asynchronously. Call `getOrders` in `viewDidLoad`, for example and in the `getOrders` completion block (where I print `responseObject`), you will (a) update your property with the results of the asynchronous call; and (b) call `[self.tableView reloadData]` from that completion closure. So, your `UITableViewDataSource` methods will end up being called twice, once when you first load it (at which point no data is yet available); and again after you call `reloadData`. – Rob Dec 21 '15 at 16:58
  • Worked wonderfully, thank you! Do I have to write a separate completion handler for each function I have? (I will have over 10 total in my application) – zerohedge Dec 21 '15 at 21:26
  • Yes, wherever you call asynchronous method, yes, you'd generally employ this pattern. – Rob Dec 21 '15 at 22:59
  • 1
    Your code looking good but give me this error. Command failed due to signal: Segmentation fault: 11 why so ? – Mitesh Jain Jan 11 '16 at 15:53
  • @Rob I am currently experiencing the same proble as Mitesh. How can you use breakpoints to Debug segmentation fault when the app is not running (this causes build fail) – Jesus Rodriguez Jan 11 '16 at 23:41
  • 2
    @Granola - Ah, I misunderstood. Xcode gave you that segmentation fault, not your app. It looks like the updated Alamofire syntax causes problems for the compiler. You can use `switch` statement to check for `.Success` and `.Failure` independently, and that gets around that problem. It's not as compact, but is even more clear about the handling of the Alamofire `Result` `enum`. – Rob Jan 12 '16 at 00:42
  • 1
    @Mitesh - See revised example above. It looks like change in Alamofire was causing a crash in Xcode. I've revised my answer to get around this Xcode bug. – Rob Jan 12 '16 at 00:44
  • @Rob I have tested the corrected code and works flawlessly now. – Jesus Rodriguez Jan 12 '16 at 02:45
  • @Rob I also have test updated code and it's working fine. Many thanks for your quick update – Mitesh Jain Jan 12 '16 at 06:03
  • 2
    perfect! thanks for this. My code is now lot cleaner and correct. For all async calls, this should be the way. Just a change if you want to parse the response as a json: instead of ```completionHandler(value as? NSDictionary, nil)``` just do ```completionHandler(value, nil)``` and replace ```NSDictionary``` in ```getOrders``` and ```makeCall``` with ```AnyObject```. value is the responseObject as it is. You can now parse this value in the handler like this: ```let json = JSON(responseObject!)``` – kishorer747 May 28 '16 at 07:48
  • @kishorer747 : where do i use the json = JSON(responseObject!) exactly?? Sorry i'm new to this.Thanx! – Jenita _Alice4Real Oct 12 '16 at 10:36
  • 1
    @Jenita_Alice4Real - I'm not a fan of SwiftyJSON so I wouldn't add that line at all. But, if you were to use it, you'd often do that right before you call the closure, and you'd change that parameter if the closure from a dictionary to a `JSON`. – Rob Oct 12 '16 at 14:57
  • @Jenita_Alice4Real here is the answer to use SwiftyJSON to parse a json: http://stackoverflow.com/a/40012727/2177085 – kishorer747 Oct 13 '16 at 05:10
  • Can i ask why does this answer use 2 completion handlers rather than one? Since this would still work just by calling the makeCall function. Just curious – Tunds Dec 28 '16 at 19:44
  • You should ask the OP, since that part was taken from his question. I'm assuming that there were details in `getOrders` that were omitted for the sake of brevity, not relevant to the immediate question. It's not unusual, though, to have one method doing low network stuff (building some standard `POST` request, handling activity indicators, etc.) and another handling application-level stuff (e.g. choosing endpoint, serializing JSON into objects, etc.). As it is, `getOrders` is offering little additional value, but I suspect in his real app, it was doing some more meaningful stuff there. – Rob Dec 28 '16 at 19:57
14

From the Alamofire README (emphasis added):

Networking in Alamofire is done asynchronously. Asynchronous programming may be a source of frustration to programmers unfamiliar with the concept, but there are very good reasons for doing it this way.

Rather than blocking execution to wait for a response from the server, a callback is specified to handle the response once it's received. The result of a request is only available inside the scope of a response handler. Any execution contingent on the response or data received from the server must be done within a handler.

mattt
  • 19,544
  • 7
  • 73
  • 84
3

Following is the complete flow for performing the 'Login Action' using Alamofire and Swift.

Alamofire v3.3 Swift 2.2 Xcode 7.3

I have used GCD and MBProgressHUD for my own convenience. Refactor and use as you like :)

func loginBtnTapped(sender: AnyObject) {

    MBProgressHUD.showHUDAddedTo(self.view, animated: true)

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {

        let loginInfo : Dictionary<String,AnyObject> = ["email":"abc@g.com","password":"abc123"]

        self.loginUser(loginInfo) { responseObject, error in

            print("\(responseObject) \n  \(error) ")

            // Parsing JSON Below
            let status = Int(responseObject?.objectForKey("status") as! String)
            if status == 1 {
                // Login Successfull...Move To New VC
            }
            else {
                print(responseObject?.objectForKey("message"))! as! String)
            }
            return
        }
        dispatch_async(dispatch_get_main_queue()) {
            MBProgressHUD.hideHUDForView(self.view, animated: true)
        }
    }

}


func loginUser(parameters:NSDictionary, completionHandler: (NSDictionary?, NSError?) -> ()) {

    self.postRequest("http://qa.company.com/project/index.php/user/login",
                     paramDict: parameters as? Dictionary<String, AnyObject>,
                     completionHandler: completionHandler)
}

func postRequest(urlString: String, paramDict:Dictionary<String, AnyObject>? = nil,
                 completionHandler: (NSDictionary?, NSError?) -> ()) {

    Alamofire.request(.POST, urlString, parameters: paramDict)
        .responseJSON { response in
            switch response.result {
            case .Success(let JSON):
                completionHandler(JSON as? NSDictionary, nil)
            case .Failure(let error):
                completionHandler(nil, error)
            }
    }

}
n.by.n
  • 2,458
  • 22
  • 31
3

Details

xCode 9.1, Swift 4

Features:

  • Easy readable code
  • Ready templates (it's easy to add more requests)
  • Embedded solution with asynchronous data processing
  • Full examples

Sample 1

Return data using closure

Data1.searchRequest(term: "jack johnson") { json, error  in
     print(error ?? "nil")
     print(json ?? "nil")
     print("Update views")
}

Full sample 1

Data class

import Alamofire

class Data1 {

    static fileprivate let queue = DispatchQueue(label: "requests.queue", qos: .utility)
    static fileprivate let mainQueue = DispatchQueue.main

    fileprivate class func make(request: DataRequest, closure: @escaping (_ json: [String: Any]?, _ error: Error?)->()) {
        request.responseJSON(queue: Data1.queue) { response in

            // print(response.request ?? "nil")  // original URL request
            // print(response.response ?? "nil") // HTTP URL response
            // print(response.data ?? "nil")     // server data
            //print(response.result ?? "nil")   // result of response serialization

            switch response.result {
            case .failure(let error):
                Data1.mainQueue.async {
                    closure(nil, error)
                }

            case .success(let data):
                Data1.mainQueue.async {
                    closure((data as? [String: Any]) ?? [:], nil)
                }
            }
        }
    }

    class func searchRequest(term: String, closure: @escaping (_ json: [String: Any]?, _ error: Error?)->()) {
        let request = Alamofire.request("https://itunes.apple.com/search?term=\(term.replacingOccurrences(of: " ", with: "+"))")
        Data1.make(request: request) { json, error in
            closure(json, error)
        }
    }
}

UIViewController

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        Data1.searchRequest(term: "jack johnson") { json, error  in
            print(error ?? "nil")
            print(json ?? "nil")
            print("Update views")
        }
    }
}

Sample 2

Return data using delegate

// ....
var data = Data2()
data.delegate = self
data.searchRequest(term: "jack johnson")
// ....

extension ViewController: Data2Delegate {
    func searchRequest(response json: [String : Any]?, error: Error?) {
        print(error ?? "nil")
        print(json ?? "nil")
        print("Update views")
    }
}

Full sample 2

Data class

import Alamofire

protocol Data2Delegate: class {
    func searchRequest(response json: [String: Any]?, error: Error?)
}

class Data2 {

    fileprivate let queue = DispatchQueue(label: "requests.queue", qos: .utility)
    fileprivate let mainQueue = DispatchQueue.main

    weak var delegate: Data2Delegate?

    fileprivate func make(request: DataRequest, closure: @escaping (_ json: [String: Any]?, _ error: Error?)->()) {
        request.responseJSON(queue: queue) { response in

            // print(response.request ?? "nil")  // original URL request
            // print(response.response ?? "nil") // HTTP URL response
            // print(response.data ?? "nil")     // server data
            //print(response.result ?? "nil")   // result of response serialization

            switch response.result {
            case .failure(let error):
                self.mainQueue.async {
                    closure(nil, error)
                }

            case .success(let data):
                self.mainQueue.async {
                    closure((data as? [String: Any]) ?? [:], nil)
                }
            }
        }
    }

    func searchRequest(term: String) {
        let request = Alamofire.request("https://itunes.apple.com/search?term=\(term.replacingOccurrences(of: " ", with: "+"))")
        make(request: request) { json, error in
            self.delegate?.searchRequest(response: json, error: error)
        }
    }
}

UIViewController

import UIKit

class ViewController: UIViewController {
    private var data = Data2()
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        data.delegate = self
        data.searchRequest(term: "jack johnson")
    }
}

extension ViewController: Data2Delegate {
    func searchRequest(response json: [String : Any]?, error: Error?) {
        print(error ?? "nil")
        print(json ?? "nil")
        print("Update views")
    }
}

Sample 3

Return data using PromiseKit

_ = data.searchRequest(term: "jack johnson").then { response in
      print(response.error ?? "nil")
      print(response.json ?? "nil")
      print("Update views")
      return .void
}

Full sample 3

Data class import Alamofire import PromiseKit

class Data3 {

    fileprivate let queue = DispatchQueue(label: "requests.queue", qos: .utility)
    fileprivate let mainQueue = DispatchQueue.main

    fileprivate func make(request: DataRequest) -> Promise<(json:[String: Any]?, error: Error?)> {
         return Promise { fulfill, reject in
            request.responseJSON(queue: queue) { response in

                // print(response.request ?? "nil")  // original URL request
                // print(response.response ?? "nil") // HTTP URL response
                // print(response.data ?? "nil")     // server data
                //print(response.result ?? "nil")   // result of response serialization

                switch response.result {
                    case .failure(let error):
                        self.mainQueue.async {
                            fulfill((nil, error))
                        }

                    case .success(let data):
                        self.mainQueue.async {
                            fulfill(((data as? [String: Any]) ?? [:], nil))
                        }
                }
            }
        }
    }

    func searchRequest(term: String) -> Promise<(json:[String: Any]?, error: Error?)> {
        let request = Alamofire.request("https://itunes.apple.com/search?term=\(term.replacingOccurrences(of: " ", with: "+"))")
        return make(request: request)
    }
}

extension AnyPromise {

    class var void: AnyPromise {
        return AnyPromise(Promise<Void>())
    }
}

UIViewController

import UIKit
import PromiseKit

class ViewController: UIViewController {
    private var data = Data3()
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        _ = data.searchRequest(term: "jack johnson").then { response in
            print(response.error ?? "nil")
            print(response.json ?? "nil")
            print("Update views")
            return .void
        }
    }
}
Vasily Bodnarchuk
  • 24,482
  • 9
  • 132
  • 127
  • i like sample 1 but i confuse .. how to insert headers, body and method ? – MAS. John Dec 21 '17 at 06:25
  • All examples are complete and working. Create a new project and experiment with the code. Add new queries to the `date class`. And usage code placed in `UIViewController class` – Vasily Bodnarchuk Dec 21 '17 at 06:32
1

To parse a json using Swifty JSON, here is how i am doing it.

For @Jenita _Alice4Real

func uploadScans(parameters: [String: AnyObject], completionHandler: (AnyObject?, NSError?) -> ()) {
    makePostCall(CommonFunctions().getSaveSKUDataUrl(), parameters: parameters,completionHandler: completionHandler)
}

func makePostCall(url: String, parameters: [String: AnyObject], completionHandler: (AnyObject?, NSError?) -> ()) {
    Alamofire.request(.POST, url, parameters: parameters)
        .responseJSON { response in
            switch response.result {
                case .Success(let value):
                    completionHandler(value, nil)
                case .Failure(let error):
                    completionHandler(nil, error)
            }
    }
}

uploadScans(params) { responseObject, error in
    let json = JSON(responseObject!)
}
kishorer747
  • 810
  • 1
  • 10
  • 24