Swift: OperationQueue - дождитесь завершения асинхронных вызовов, прежде чем начинать следующий (не более 2 одновременно выполняемых URL-запросов)

tl;dr У меня есть OperationQueue, и я хочу, чтобы одновременно выполнялись две операции. Эти операции загружают что-то асинхронно, поэтому все они запускаются сразу, а не запускаются одна за другой.

Я заполняю таблицу очень больших изображений, выполняя следующие действия для каждого из изображений:

public func imageFromUrl(_ urlString: String) {
    if let url = NSURL(string: urlString) {
        let request = NSURLRequest(url: url as URL)
        let config = URLSessionConfiguration.default
        let session = URLSession(configuration: config)

        let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
            if let imageData = data as Data? {
                DispatchQueue.main.async {
                    self.setImageData(imageData)
                }
            }
        });

        task.resume()
    }

называя это как imageView.imageFromUrl(...),

При более медленном интернет-соединении звонки складываются, и он начинает загружать каждое изображение сразу. Затем пользователь должен подождать, пока загрузки "сразятся" друг с другом, и некоторое время смотрит на пустой экран, прежде чем все изображения появляются сразу (более или менее). Для пользователя было бы намного лучше, если бы одно изображение появлялось за другим.

Я думал о постановке в очередь элементов, загрузке первого из списка, удалении его из списка и рекурсивном вызове функции следующим образом:

func doImageQueue(){

    let queueItem = imageQueue[0]

    if let url = NSURL(string: (queueItem.url)) {
        print("if let url")
        let request = NSURLRequest(url: url as URL)
        let config = URLSessionConfiguration.default
        let session = URLSession(configuration: config)

        let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
            print("in")
            if let imageData = data as Data? {
                print("if let data")
                DispatchQueue.main.async {
                    queueItem.imageView.setImageData(imageData)
                    self.imageQueue.remove(at: 0)
                    if(self.imageQueue.count>0) {
                        self.doImageQueue()
                    }
                }
            }
        });

        task.resume()
    }
}

Это действительно загружает изображения один за другим, так как я думаю, что это пустая трата времени - не запускать как минимум 2 запроса одновременно. Создание моей текущей реализации, обрабатывающей 2 изображения одновременно, привело бы к большому коду спагетти, поэтому я изучил Swift OperationQueue,

я бы сделал

let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2

for (all images) {
    queue.addOperation {
        imageView.imageFromUrl(imageURL
    }
}

Но это также инициирует все вызовы одновременно, возможно, из-за того, что запросы выполняются асинхронно, и вызов метода заканчивается до ожидания загрузки изображения. Как я могу справиться с этим? Приложение также будет работать на watchOS, может быть, уже есть библиотека для этого, но я не думаю, что это будет слишком сложно достичь без библиотеки. Кеширование не является проблемой.

1 ответ

Решение

Ваш оригинальный код с очередью операций и ваш оригинальный iamgeFromUrl метод это все, что вам нужно, если вы сделаете одно небольшое изменение imageFromUrl, Вам нужно добавить пару строк кода, чтобы imageFromUrl не возвращается, пока загрузка не будет завершена.

Это можно сделать с помощью семафора.

public func imageFromUrl(_ urlString: String) {
    if let url = URL(string: urlString) {
        let request = URLRequest(url: url)
        let config = URLSessionConfiguration.default
        let session = URLSession(configuration: config)

        let semaphore = DispatchSemaphore(value: 0)
        let task = session.dataTask(with: request, completionHandler: {(data, response, error) in
            semaphore.signal()
            if let imageData = data as Data? {
                DispatchQueue.main.async {
                    self.setImageData(imageData)
                }
            }
        });

        task.resume()
        semaphore.wait()
    }
}

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

Также обратите внимание, что код изменен, чтобы избежать использования NSURL а также NSURLRequest,

Другие вопросы по тегам