链接多个Alamofire请求

53

我正在寻找一种好的模式,可以链接多个HTTP请求。我想使用Swift,并且最好使用Alamofire

比如说,我想要做以下操作:

  1. 发出PUT请求
  2. 发出GET请求
  3. 用数据重新加载表格

似乎promises的概念很适合这种情况。如果我能做到以下操作,PromiseKit可能是一个不错的选择:

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
}

但这似乎不可能,或者至少我不知道怎么做。

如何在不嵌套多个方法的情况下实现此功能?

我是iOS新手,也许有些基本的东西我还不懂。在其他框架(比如Android)中,我会在后台进程中执行这些操作,并进行同步请求。但是Alamofire是异步的,所以这种模式不可行。


我没有使用过PromiseKit,但是另一种选择是使用AFNetworking的AFHTTPRequestOperation,你可以将其放在一个NSOperationQueue中。你可以设置操作只有在其他操作完成后才能开始。 - Aaron Brager
你应该能够使用PromiseKit,虽然你必须为它提供自己的支持,最明显的方法是将其作为对AlamoFire.request的扩展。看看他们为NSURLConnection所做的并以此作为模型。 - David Berry
你可以使用 ReactiveCocoa 代替 PromiseKit。ReactiveCocoa 可以被视为 PromiseKit 的超集,因为它提供了更多的功能,可以在更多的地方使用,简化了你的代码结构等等。 - gkaimakas
7个回答

45

将其他异步操作包装在 Promise 中的方式如下:

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

编辑:现在使用:https://github.com/PromiseKit/Alamofire-


9
可以举一个使用案例的例子吗?也许可以实现在问题中发布的请求? - Bernardo Santana
如果前一个请求的响应需要作为下一个请求的输入,我们该如何处理链式调用? - msmq

31

我写了一个处理一系列请求的类。

我创建了一个名为RequestChain的类,它以Alamofire.Request作为参数。

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
        }

    }
}

我是这样使用它的

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


}

重要的是,您要告诉经理不要立即执行请求。

    let manager = Manager.sharedInstance
    manager.startRequestsImmediately = false

希望能帮助到其他人

Swift 3.0 更新

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
        }

    }
}

使用示例 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

        }

这非常酷,并且非常优雅地解决了我遇到的问题。现在在运行Swift 3时,它会抱怨request.response(completionHandler: {(_, _, _, error)给出错误“Cannot call value of non-function type HTTPURLResponse?”。谢谢。 - iphaaw
嗨@Eike,你能否添加一个Swift3类的使用示例吗?谢谢! - Besi
最佳答案,绝对是最面向对象的。谢谢 :) - Hernan Arber
最佳方法是我尝试在Swift 4中添加它,但它总是在request.response(completionHandler: { (_, _, _, error)处失败。与iPhaaw之前遇到的问题相同。 - Anjan
@Eike 谢谢你的回答,它确实帮了我很大的忙。但是我还有另一个问题,当所有请求都完成后,我更新了我的UI,但我无法发出另一个alamofire请求。 - Mansuu....
显示剩余2条评论

18

您有多种选择。


选项1 - 嵌套调用

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

这绝对是我推荐的方法。在一个调用中嵌套另一个调用非常简单,而且很容易理解。它还可以保持事情简单。


选项2 - 拆分为多个方法

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
}

这个选项更加简洁,将内容分成较小的块。根据您的需求和响应解析的复杂程度,这可能是一种更易读的方法。


选项3 - PromiseKit和Alamofire

Alamofire可以轻松处理此操作,无需引入PromiseKit。如果您真的想选择这个选项,您可以使用@mxcl提供的方法。


选项3可以与@mxcl的答案相补充。 - jlhonora
你的前两个选项涉及嵌套,而 Promise 的设计就是为了避免这种情况。所以我不确定说 Alamofire 处理得很好是否有意义。难道你真的是在说嵌套不是问题吗? - Ian Warburton
2
我在我的回答中没有看到任何地方说Alamofire处理这个问题“非常好”。我只是指出了三种不同的完成任务的选项。由于我不是PromiseKit的专家,我想提供仅使用Alamofire的两个选项,并将第三个推迟到PromiseKit直接处理。使用Alamofire可以直接链接两个请求。超过两个请求,它开始变得相当难以控制。这是我们未来肯定要调查的事情。 - cnoon
但如果您在for循环中进行多次调用,我如何知道最后一次调用何时完成? - Cristian Cardoso
选项1可能无法实现所需的功能...一旦嵌套的Alamofire获取请求开始,函数就会返回。 - Dale

7

这里有另一种使用DispatchGroup来实现的方法(Swift 3,Alamofire 4.x),代码如下:

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

        })

    }
}

这看起来非常优雅,但是如何在 notify 调用中收集数据呢? - Besi
在请求调用之前,简单地声明将保存数据的变量,使用每个请求填充它,并在通知调用中对该变量执行某些操作(此时它将从请求数据中填充)。顺便说一句,我明天会更新答案中的代码(我找到了一种更可靠的方法来串联请求)... - Etienne Beaule
刚刚更新了答案,加入了一个更详细(且复杂)的例子。我查看了 PromiseKit 但觉得它仅为了实现链式请求而增加了太多复杂性... - Etienne Beaule
2
这就是Dispatch组的用途。这是更好的答案,因为它教给你一个很有用的概念,以备将来(当你进入严肃的多线程编程)之用。 - xaphod
1
我无法使用三个 Alamofire 请求使其正常工作...通知过早运行。 - Dale
显示剩余3条评论

2

详情

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

完整示例

网络服务

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

主函数

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

结果

输入图像描述

这是一个图片链接。

0

你可以使用 PromiseKit 中的 when 方法来附加/追加任意多个调用。

以下是来自 PromiseKit docs 的示例:

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

对我来说它完美地工作了,而且是一个更清晰的解决方案。


-1

无限调用自身并定义结束条件。API链接的urlring和json字典的Dictionary。

我们可以构建队列模型或委托。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接