53

I'm looking for a good pattern with which I can chain multiple HTTP requests. I want to use Swift, and preferrably Alamofire.

Say, for example, I want to do the following:

  1. Make a PUT request
  2. Make a GET request
  3. Reload table with data

It seems that the concept of promises may be a good fit for this. PromiseKit could be a good option if I could do something like this:

NSURLConnection.promise(
    Alamofire.request(
        Router.Put(url: "http://httbin.org/put")
    )
).then { (request, response, data, error) in
    Alamofire.request(
        Router.Get(url: "http://httbin.org/get")
    )   
}.then { (request, response, data, error) in
    // Process data
}.then { () -> () in
    // Reload table
}

but that's not possible or at least I'm not aware of it.

How can I achieve this functionality without nesting multiple methods?

I'm new to iOS so maybe there's something more fundamental that I'm missing. What I've done in other frameworks such as Android is to perform these operations in a background process and make the requests synchronous. But Alamofire is inherently asynchronous, so that pattern is not an option.

jlhonora
  • 10,179
  • 10
  • 46
  • 70
  • I haven't used PromiseKit, but alternative would be to use AFNetworking's [`AFHTTPRequestOperation`](http://cocoadocs.org/docsets/AFNetworking/2.4.1/Classes/AFHTTPRequestOperation.html) which you can put in an `NSOperationQueue`. You can set the operations to only start as other operations have completed. – Aaron Brager Feb 20 '15 at 19:02
  • You should be able to use `PromiseKit` although you'll have to provide your own support for it, the obvious way would be as an extension to `AlamoFire.request` Checkout what they've done for `NSURLConnection` and use that as a model. – David Berry Feb 20 '15 at 21:25
  • You could use ReactiveCocoa instead of PromiseKit. ReactiveCocoa can be seen as a superset of PromiseKit since it provides much more functionality, can be used in many more places, streamlines your code structure and much more – gkaimakas Feb 18 '16 at 09:30

7 Answers7

45

Wrapping other asynchronous stuff in promises works like this:

func myThingy() -> Promise<AnyObject> {
    return Promise{ fulfill, reject in
        Alamofire.request(.GET, "http://httpbin.org/get", parameters: ["foo": "bar"]).response { (_, _, data, error) in
            if error == nil {
                fulfill(data)
            } else {
                reject(error)
            }
        }
    }
}

Edit: Nowadays, use: https://github.com/PromiseKit/Alamofire-

mxcl
  • 26,392
  • 12
  • 99
  • 98
31

I wrote a class which handles a chain of request one by one.

I created a class RequestChain wich takes Alamofire.Request as parameter

class RequestChain {
    typealias CompletionHandler = (success:Bool, errorResult:ErrorResult?) -> Void

    struct ErrorResult {
        let request:Request?
        let error:ErrorType?
    }

    private var requests:[Request] = []

    init(requests:[Request]) {
        self.requests = requests
    }

    func start(completionHandler:CompletionHandler) {
        if let request = requests.first {
            request.response(completionHandler: { (_, _, _, error) in
                if error != nil {
                    completionHandler(success: false, errorResult: ErrorResult(request: request, error: error))
                    return
                }
                self.requests.removeFirst()
                self.start(completionHandler)
            })
            request.resume()
        }else {
            completionHandler(success: true, errorResult: nil)
            return
        }

    }
}

And I use it like this

let r1 = Alamofire.request(Router.Countries).responseArray(keyPath: "endpoints") { (response: Response<[CountryModel],NSError>) in
    print("1")
}

let r2 = Alamofire.request(Router.Countries).responseArray(keyPath: "endpoints") { (response: Response<[CountryModel],NSError>) in
    print("2")
}

let r3 = Alamofire.request(Router.Countries).responseArray(keyPath: "endpoints") { (response: Response<[CountryModel],NSError>) in
    print("3")
}

let chain = RequestChain(requests: [r1,r2,r3])

chain.start { (success, errorResult) in
    if success {
        print("all have been success")
    }else {
        print("failed with error \(errorResult?.error) for request \(errorResult?.request)")
    }


}

Importent is that you are telling the Manager to not execute the request immediately

    let manager = Manager.sharedInstance
    manager.startRequestsImmediately = false

Hope it will help someone else

Swift 3.0 Update

class RequestChain {
    typealias CompletionHandler = (_ success:Bool, _ errorResult:ErrorResult?) -> Void

    struct ErrorResult {
        let request:DataRequest?
        let error:Error?
    }

    fileprivate var requests:[DataRequest] = []

    init(requests:[DataRequest]) {
        self.requests = requests
    }

    func start(_ completionHandler:@escaping CompletionHandler) {
        if let request = requests.first {
            request.response(completionHandler: { (response:DefaultDataResponse) in
                if let error = response.error {
                    completionHandler(false, ErrorResult(request: request, error: error))
                    return
                }

                self.requests.removeFirst()
                self.start(completionHandler)
            })
            request.resume()
        }else {
            completionHandler(true, nil)
            return
        }

    }
}

Usage Example Swift 3

/// set Alamofire default manager to start request immediatly to false
        SessionManager.default.startRequestsImmediately = false
        let firstRequest = Alamofire.request("https://httpbin.org/get")
        let secondRequest = Alamofire.request("https://httpbin.org/get")

        let chain = RequestChain(requests: [firstRequest, secondRequest])
        chain.start { (done, error) in

        }
Eike
  • 2,311
  • 20
  • 31
  • This is very cool and fixed a problem I was having very elegantly. It's now complaining when running in Swift 3 request.response(completionHandler: { (_, _, _, error) giving an error "Cannot call value of non-function type HTTPURLResponse?". Thanks. – iphaaw Oct 02 '16 at 08:49
  • Hy @Eike, could you maybe add an example of how the swift3 class is then used? Thanks! – Besi Dec 11 '16 at 23:27
  • Best Answer, definitely most OOP. THANKS :) – Hernan Arber Apr 05 '17 at 07:39
  • Best approach but i try to add it in swift 4 and it always fall in request.response(completionHandler: { (_, _, _, error). same issue as iPhaaw face earlier. – Anjan Apr 05 '18 at 19:35
  • How would you get some data pulled from the first response onto the second request? – Dale May 19 '18 at 20:37
  • @Eike How do I get error mapping and response for each of the request separately? Is that possible? – denis_lor Oct 17 '18 at 08:41
  • @Eike Thanks for the answer, It really helped me. But I am having another problem, when all requests get finished I updated my UI, but I am not able to make another alamofire request. – Mansuu.... Feb 14 '19 at 09:03
18

You have multiple options.


Option 1 - Nesting Calls

func runTieredRequests() {
    let putRequest = Alamofire.request(.PUT, "http://httpbin.org/put")
    putRequest.response { putRequest, putResponse, putData, putError in
        let getRequest = Alamofire.request(.GET, "http://httpbin.org/get")
        getRequest.response { getRequest, getResponse, getData, getError in
            // Process data
            // Reload table
        }
    }
}

This is definitely the approach I would recommend. Nesting one call into another is very simple and is pretty easy to follow. It also keeps things simple.


Option 2 - Splitting into Multiple Methods

func runPutRequest() {
    let putRequest = Alamofire.request(.PUT, "http://httpbin.org/put")
    putRequest.response { [weak self] putRequest, putResponse, putData, putError in
        if let strongSelf = self {
            // Probably store some data
            strongSelf.runGetRequest()
        }
    }
}

func runGetRequest() {
    let getRequest = Alamofire.request(.GET, "http://httpbin.org/get")
    getRequest.response { [weak self] getRequest, getResponse, getData, getError in
        if let strongSelf = self {
            // Probably store more data
            strongSelf.processResponse()
        }
    }
}

func processResponse() {
    // Process that data
}

func reloadData() {
    // Reload that data
}

This option is less dense and splits things up into smaller chunks. Depending on your needs and the complexity of your response parsing, this may be a more readable approach.


Option 3 - PromiseKit and Alamofire

Alamofire can handle this pretty easily without having to pull in PromiseKit. If you really want to go this route, you can use the approach provided by @mxcl.

cnoon
  • 16,575
  • 7
  • 58
  • 66
  • Option 3 could be complemented with @mxcl's answer – jlhonora Mar 12 '15 at 16:23
  • Your first two options involve nesting which is what promises are designed to avoid. So I'm not sure it makes sense to say that Alamofire can handle this pretty well. Aren't you really saying that nesting isn't a problem? – Ian Warburton Oct 26 '15 at 19:50
  • 2
    I don't see anywhere in my answer that says Alamofire handles this "pretty well". I merely pointed out three different options for accomplishing the task. Not being an expert in PromiseKit, I figured I'd provide a couple options using Alamofire only, with a third deferring to PromiseKit directly. Chaining two requests together can easily be done directly with Alamofire. More than two and it starts to get pretty unwieldy. This is something we're certainly going to investigate in the future. – cnoon Oct 27 '15 at 03:56
  • But if you make many calls in a for, how can I know when the last call it is completed ? – Cristian Cardoso Nov 18 '16 at 00:21
  • Option 1 might not do what is desired... As soon as the nested Alamofire get request starts, the function returns. – Dale May 19 '18 at 21:04
7

Here is another way to do this (Swift 3, Alamofire 4.x) using a DispatchGroup

import Alamofire

    struct SequentialRequest {

        static func fetchData() {

            let authRequestGroup =  DispatchGroup()
            let requestGroup = DispatchGroup()
            var results = [String: String]()

            //First request - this would be the authentication request
            authRequestGroup.enter()
            Alamofire.request("http://httpbin.org/get").responseData { response in
            print("DEBUG: FIRST Request")
            results["FIRST"] = response.result.description

            if response.result.isSuccess { //Authentication successful, you may use your own tests to confirm that authentication was successful

                authRequestGroup.enter() //request for data behind authentication
                Alamofire.request("http://httpbin.org/get").responseData { response in
                    print("DEBUG: SECOND Request")
                    results["SECOND"] = response.result.description

                    authRequestGroup.leave()
                }

                authRequestGroup.enter() //request for data behind authentication
                Alamofire.request("http://httpbin.org/get").responseData { response in
                    print("DEBUG: THIRD Request")
                    results["THIRD"] = response.result.description

                    authRequestGroup.leave()
                }
            }

            authRequestGroup.leave()

        }


        //This only gets executed once all the requests in the authRequestGroup are done (i.e. FIRST, SECOND AND THIRD requests)
        authRequestGroup.notify(queue: DispatchQueue.main, execute: {

            // Here you can perform additional request that depends on data fetched from the FIRST, SECOND or THIRD requests

            requestGroup.enter()
            Alamofire.request("http://httpbin.org/get").responseData { response in
                print("DEBUG: FOURTH Request")
                results["FOURTH"] = response.result.description

                requestGroup.leave()
            }


            //Note: Any code placed here will be executed before the FORTH request completes! To execute code after the FOURTH request, we need the request requestGroup.notify like below
            print("This gets executed before the FOURTH request completes")

            //This only gets executed once all the requests in the requestGroup are done (i.e. FORTH request)
            requestGroup.notify(queue: DispatchQueue.main, execute: {

                //Here, you can update the UI, HUD and turn off the network activity indicator

                for (request, result) in results {
                    print("\(request): \(result)")
                }

                print("DEBUG: all Done")
            })

        })

    }
}
Etienne Beaule
  • 207
  • 2
  • 10
  • This looks very elegant, but how can the data be collected within the `notify` call? – Besi Dec 11 '16 at 23:28
  • Simply declare the variable that will hold the data before the request calls, populate it with each request and do something with the variable in the notify call (it will be populated from the request data at that time). BTW, I will update the code in the answer tomorrow (I found a more reliable way to daisy chain the requests)... – Etienne Beaule Dec 11 '16 at 23:37
  • I used [PromiseKit](http://promisekit.org) in the past to chain such requests. I find it a very handy framework so you might want to check it out. – Besi Dec 12 '16 at 08:03
  • Just updated the answer with a more detailed (and complex) example. I did look at PromiseKit but I felt it was adding too much complexity simply for the purpose of making daisy-chained requests... – Etienne Beaule Dec 12 '16 at 12:16
  • 2
    This is what Dispatch groups are made for. This is the better answer because it teaches you a very useful concept for later (when you get into serious multithreading) – xaphod Apr 04 '17 at 20:52
  • 1
    I couldn't get this to work with three Alamofire requests...the notifies ran too soon. – Dale May 19 '18 at 21:06
  • How to extract all resultant data from notify call? – kiran Feb 01 '19 at 13:53
  • Kiran: I now prefer PromiseKit over dispatchGroup. There is a bit of a learning curve with PromiseKit but it is way easier to manage once you start having a lot of requests to daisy chain... – Etienne Beaule Feb 02 '19 at 17:47
2

Details

  • Alamofire 4.7.2
  • PromiseKit 6.3.4
  • Xcode 9.4.1
  • Swift 4.1

Full Sample

NetworkService

import Foundation
import Alamofire
import PromiseKit

class NetworkService {

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

    fileprivate class func make(request: DataRequest) -> Promise <(json: [String: Any]?, error: Error?)> {
        return Promise <(json: [String: Any]?, error: Error?)> { seal 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):
                    DispatchQueue.main.async {
                        seal.fulfill((nil, error))
                    }

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

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

Main func

func run() {
    _ = firstly {
        return Promise<Void> { seal in
            DispatchQueue.global(qos: .background).asyncAfter(deadline: DispatchTime.now() + .seconds(2)) {
                print("1 task finished")
                DispatchQueue.main.async {
                    seal.fulfill(Void())
                }
            }
        }
        }.then {
            return NetworkService.searchRequest(term: "John").then { json, error -> Promise<Void> in
                print("2 task finished")
                //print(error ?? "nil")
                //print(json ?? "nil")
                return Promise { $0.fulfill(Void())}
            }
        }.then {_ -> Promise<Bool> in
            print("Update UI")
            return Promise { $0.fulfill(true)}
        }.then { previousResult -> Promise<Void> in
            print("previous result: \(previousResult)")
            return Promise { $0.fulfill(Void())}
    }
}

Result

enter image description here

Vasily Bodnarchuk
  • 24,482
  • 9
  • 132
  • 127
0

You can use the when method in PromiseKit to attach/append as many calls you want.

Here's an example from PromiseKit docs:

firstly {
    when(fulfilled: operation1(), operation2())
}.done { result1, result2 in
    //…
}

It worked perfectly for me and it's a much cleaner solution.

-1

Call itself infinitely and DEFINE END CONDITION. urlring for API link and Dictionary for json

WE may construct the queue model or delegate

 func getData(urlring : String  , para :  Dictionary<String, String>) {

    if intCount > 0 {

        Alamofire.request( urlring,method: .post, parameters: para , encoding: JSONEncoding.default, headers: nil) .validate()
            .downloadProgress {_ in
            }
            .responseSwiftyJSON {
                dataResponse in
                switch dataResponse.result {
                case .success(let json):
                    print(json)
                    let loginStatus : String = json["login_status"].stringValue
                    print(loginStatus)
                    if  loginStatus == "Y" {
                        print("go this")
                        print("login success : int \(self.intCount)")

                        self.intCount-=1
                        self.getData(urlring: urlring , para : para)
                    }
                case .failure(let err) :
                    print(err.localizedDescription)
                }
        }
    }else{
       //end condition workout
    }
}
Jeff Bootsholz
  • 2,971
  • 15
  • 70
  • 141