Синхронизация нескольких вызовов веб-сервисов в последовательном порядке в 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
}