Синхронизация нескольких вызовов веб-сервисов в последовательном порядке в Swift

Я нажимаю URL веб-службы 10 раз и получаю ответ. я использую Alamofire а также SwiftyJSON, Это код моего контроллера

class ViewController: UIViewController {

    let dispatchGroup = DispatchGroup()

    var weatherServiceURL = "http://samples.openweathermap.org/data/2.5/weather?q=London,uk&appid=b6907d289e10d714a6e88b30761fae22"

    override func viewDidLoad() {
        super.viewDidLoad()
        start()
    }

    func start() {
        weatherService()
        dispatchGroup.notify(queue: .main) {
            print("All services complete")
        }
    }

    func weatherService() {
        for i in 1...10 {
            dispatchGroup.enter()
            APIManager.apiGet(serviceName: self.weatherServiceURL, parameters: ["counter":i]) { (response:JSON?, error:NSError?, count:Int) in
                if let error = error {
                    print(error.localizedDescription)
                    return
                }
                guard let response = response else { return }
                print("\n\(response) \n\(count) response\n")
                self.dispatchGroup.leave()
            }
        }
    }
}

Это мой код класса Service Handler

class APIManager: NSObject {

    class func apiGet(serviceName:String,parameters: [String:Any]?, completionHandler: @escaping (JSON?, NSError?, Int) -> ()) {
        Alamofire.request(serviceName, method: .get, parameters: nil, encoding: URLEncoding.default, headers: nil).responseJSON { (response:DataResponse<Any>) in

            switch(response.result) {
            case .success(_):
                if let data = response.result.value{
                    let json = JSON(data)
                    completionHandler(json,nil, parameters!["counter"] as! Int)
                }
                break

            case .failure(_):
                completionHandler(nil,response.result.error as NSError?, parameters!["counter"] as! Int)
                break
            }
        }
    }
}

Я отправляю счетчик с индексом цикла for, чтобы отслеживать, какой индекс возвращается. Но ответ идет не в серийном порядке. Мы можем ожидать 3-й ответ до 2-го и 1-го ответа. Это потому, что вызов API с APIManager.apiGet вызов функции является асинхронным и экранируется, и поэтому продолжается цикл for.

Также я использовал dispueQueue

let dispatchQueue = DispatchQueue(label: "com.test.Queue", qos: .userInteractive)

и преобразовал функцию как:

func weatherService() {
    for i in 1...10 {
        dispatchGroup.enter()
        dispatchQueue.async {
            APIManager.apiGet(serviceName: self.weatherServiceURL, parameters: ["counter":i]) { (response:JSON?, error:NSError?, count:Int) in
                if let error = error {
                    print(error.localizedDescription)
                    return
                }
                guard let response = response else { return }
                print("\n\(response) \n\(count) response\n")
                self.dispatchGroup.leave()
            }
        }
    }
}

Тот же результат, что и код вызова службы асинхронный. Если мы сделаем

dispatchQueue.sync {
   //service call 
}

тогда мы также не получим ответ в последовательном порядке, поскольку сетевой вызов в async и dispatchQueue предполагает, что задача выполнена.

Условие заключается в том, чтобы использовать сервис только в асинхронном режиме, не замораживая пользовательский интерфейс. Если я нажму на сервис синхронно, то получу желаемый результат. Но блокировка основного потока вообще не приемлема.

Я могу управлять этим с помощью массива или некоторых глобальных переменных bool, но я не хочу их использовать. Есть ли другой способ получить ответ в последовательном порядке, в котором он вызывается? Любая помощь или подсказка приветствуется.

4 ответа

Вместо того чтобы сохранять замыкания, я решил обернуть все в очередь отправки и использовать внутри нее семафоры.

//Create a dispatch queue 
let dispatchQueue = DispatchQueue(label: "myQueue", qos: .background)

//Create a semaphore
let semaphore = DispatchSemaphore(value: 0)

func weatherService() {

    dispatchQueue.async {
        for i in 1...10 {
            APIManager.apiGet(serviceName: self.weatherServiceURL, parameters: ["counter":i]) { (response:JSON?, error:NSError?, count:Int) in
                if let error = error {
                    print(error.localizedDescription)
                    return
                }
                guard let response = response else { return }
                //print("\n\(response) \n\(count) response\n")
                print("\(count) ")

                //Check by index, the last service in this case
                if i == 10 {
                    print("Services Completed")
                }

                //Signals free on service return to work for next service
                self.semaphore.signal()
            }

            //Wait till the service returns
            self.semaphore.wait()
        }
    }
    print("Start Fetching")
}

Выход это всегда

идея

  • index1 - индекс в вашем цикле, когда вы создаете замыкание
  • index2 - индекс выполненной операции в контейнере

Вам нужно создать контейнер с крышками. Этот контейнер сохранит все замыкания. Контейнер проверит, index1 == index2 выполнить все операции до index1 и после if index1 + 1 > exist,

Таким образом, этот контейнер будет проверять порядок полученных замыканий и запускать замыкания одно за другим в порядке возрастания.

подробности

Xcode 9.4.1, Swift 4.1

Контейнер

class ActionsRunController {

    typealias Func = ()->()
    private var actions: [Int: Func] = [:]
    private var dispatchSemaphore = DispatchSemaphore(value: 1)
    private var firstIndex = 0
    private var lastIndex = 0

    func add(at index: Int, action: Func?) {
        dispatchSemaphore.wait()
        actions[index] = action
        if lastIndex == index {
            while (actions[firstIndex] != nil) {
                actions[firstIndex]?()
                actions[firstIndex] = nil
                firstIndex += 1
            }
            lastIndex = firstIndex
        }
        dispatchSemaphore.signal()
    }
}

Полный код

Не забудьте добавить код Контейнера здесь

import UIKit
import Alamofire
import SwiftyJSON

class ViewController: UIViewController {

    let dispatchGroup = DispatchGroup()

    var weatherServiceURL = "http://samples.openweathermap.org/data/2.5/weather?q=London,uk&appid=b6907d289e10d714a6e88b30761fae22"

    override func viewDidLoad() {
        super.viewDidLoad()
        start()
    }

    func start() {
        weatherService()
        dispatchGroup.notify(queue: .main) {
            print("All services complete")
        }
    }

    func weatherService() {
        for i in 0...9 {
            dispatchGroup.enter()
            APIManager.apiGet(serviceName: self.weatherServiceURL, counter: i) { (response:JSON?, error:NSError?, count:Int) in
                if let error = error {
                    print(error.localizedDescription)
                    return
                }
                //guard let response = response else { return }
                print("[executed] action \(count)")
                self.dispatchGroup.leave()
            }
        }
    }
}

class APIManager: NSObject {

    private static let actionsRunController = ActionsRunController()

    class func apiGet(serviceName:String, counter:  Int, completionHandler: @escaping (JSON?, NSError?, Int) -> ()) {
        Alamofire.request(serviceName, method: .get, parameters: nil, encoding: URLEncoding.default, headers: nil).responseJSON { (response:DataResponse<Any>) in

            //print("[created] action \(counter)")
            switch(response.result) {
            case .success(_):
                if let data = response.result.value{
                    let json = JSON(data)
                    actionsRunController.add(at: counter) {
                        completionHandler(json, nil, counter)
                    }
                }
                break

            case .failure(_):
                actionsRunController.add(at: counter) {
                    completionHandler(nil,response.result.error as NSError?, counter)
                }
                break
            }
        }
    }
}

Результат

введите описание изображения здесь

Простейший способ сделать вызовы API по порядку - это выполнить "следующий" вызов в обработчике завершения предыдущего, а не использовать for цикл вне вызовов API.

func weatherService(counter: Int = 1, maxCount: Int = 10) {
    guard counter <= maxCount else {
        return
    }
    dispatchGroup.enter()
    APIManager.apiGet(serviceName: self.weatherServiceURL, parameters: ["counter":i]) { (response:JSON?, error:NSError?, count:Int) in
            self.weatherService(counter: counter+1, maxCount: maxCount)
            if let error = error {
                print(error.localizedDescription)
                self.dispatchGroup.leave()
                return
            }
            guard let response = response else {
                self.dispatchGroup.leave()
                return 
            }
            print("\n\(response) \n\(count) response\n")
            self.dispatchGroup.leave()
        }
    }
}

Я бы посоветовал против этого, хотя, если нет некоторой зависимости от порядка (то есть для вызова номер 2 требуется информация из результата вызова 1), потому что это займет больше времени, чем параллельные запросы.

Было бы гораздо лучше справиться с тем фактом, что результаты могут возвращаться не в порядке.

Кроме того, при использовании группы рассылки вам необходимо убедиться, что вы звоните dispatchGroup.leave во всех случаях, когда код завершается; в вашем случае вы этого не делаете в случае возникновения ошибки. Это приведет к dispatchGroup.notify никогда не срабатывает, если в одном или нескольких запросах возникает ошибка.

код, который можно легко использовать в нескольких местах:

      extension Array where Element:Equatable {
func syncedExecution(execute:@escaping ((Element, @escaping (() -> Void)) -> ()), completion: (() -> Void)? = nil) {
    let dispatchQueue = DispatchQueue(label: UUID().uuidString, qos: .background)
    let dispatchSemaphore = DispatchSemaphore(value: 0)
    dispatchQueue.async {
        for item in self {
            execute(item) {
                dispatchSemaphore.signal()
                if item == last {
                    completion?()
                }
            }
            dispatchSemaphore.wait()
        }
    }
}
}

Применение:

      itemsArr. syncedExecution { item, signal in
    item.someApiRequest { success in
        // do something
        signal()
    }
} completion: {
        // all tasks completed
    }
Другие вопросы по тегам