Есть ли способ запросить несколько различных ресурсов параллельно, используя URLSession.shared.dataTask - PullRequest
1 голос
/ 12 мая 2019

Я нашел здесь этот фрагмент кода о том, как загружать изображения одновременно без каких-либо поломок,

    func loadImageRobsAnswer(with urlString: String?) {
    // cancel prior task, if any


    weak var oldTask = currentTask
    currentTask = nil
    oldTask?.cancel()



    // reset imageview's image

    self.image = nil

    // allow supplying of `nil` to remove old image and then return immediately

    guard let urlString = urlString else { return }

    // check cache



    if let cachedImage = DataCache.shared.object(forKey: urlString) {



        self.transition(toImage: cachedImage as? UIImage)
        //self.image = cachedImage
        return
    }

    // download

    let url = URL(string: urlString)!
    currentURL = url

    let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
        self?.currentTask = nil



        if let error = error {


            if (error as NSError).domain == NSURLErrorDomain && (error as NSError).code == NSURLErrorCancelled {
                return
            }

            print(error)
            return
        }

        guard let data = data, let downloadedImage = UIImage(data: data) else {
            print("unable to extract image")
            return
        }

        DataCache.shared.saveObject(object: downloadedImage, forKey: urlString)

        if url == self?.currentURL {

            DispatchQueue.main.async {

                self?.transition(toImage: downloadedImage)

            }
        }
    }

    // save and start new task

    currentTask = task
    task.resume()
}

Однако этот код используется в расширении UIImageView,

    public extension UIImageView {
  private static var taskKey = 0
  private static var urlKey = 0

  private var currentTask: URLSessionTask? {
    get { return objc_getAssociatedObject(self, &UIImageView.taskKey) as? URLSessionTask }
    set { objc_setAssociatedObject(self, &UIImageView.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}

private var currentURL: URL? {
    get { return objc_getAssociatedObject(self, &UIImageView.urlKey) as? URL }
    set { objc_setAssociatedObject(self, &UIImageView.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}}}

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

class DataRequest {
private static var taskKey = 0
private static var urlKey = 0
static let shared = DataRequest()
    typealias ImageDataCompletion = (_ image: UIImage?, _ error: Error? ) -> Void

private var currentTask: URLSessionTask? {
    get { return objc_getAssociatedObject(self, &DataRequest.taskKey) as? URLSessionTask }
    set { objc_setAssociatedObject(self, &DataRequest.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}

private var currentURL: URL? {
    get { return objc_getAssociatedObject(self, &DataRequest.urlKey) as? URL }
    set { objc_setAssociatedObject(self, &DataRequest.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}


 func downloadImage(with urlString: String?, completion: @escaping ImageDataCompletion) {



    weak var oldTask = currentTask
    currentTask = nil
    oldTask?.cancel()





    guard let urlString = urlString else { return }





    if let cachedImage = DataCache.shared.object(forKey: urlString) {
         DispatchQueue.main.async {
        completion(cachedImage as? UIImage ,nil)
        }
       // self.transition(toImage: cachedImage as? UIImage)
        //self.image = cachedImage
        return
    }

    // download

    let url = URL(string: urlString)!
    currentURL = url

    let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
        self?.currentTask = nil



        if let error = error {


            if (error as NSError).domain == NSURLErrorDomain && (error as NSError).code == NSURLErrorCancelled {
                return
            }

             completion(nil,nil)
            return
        }

        guard let data = data, let downloadedImage = UIImage(data: data) else {
            print("unable to extract image")
            return
        }

        DataCache.shared.saveObject(object: downloadedImage, forKey: urlString)

        if url == self?.currentURL {

            DispatchQueue.main.async {

                 completion(downloadedImage ,nil)

            }
        }
    }

    // save and start new task

    currentTask = task
    task.resume()
}

Так что теперь я могу использовать его в расширении UIImageview, как это

    extension UIImageView {
       func setImage(url: String?) {

    self.image = nil
    DataRequest.shared.downloadImage(with: url) { (image, error) in
        DispatchQueue.main.async {
            self.image = image


        }
    }

}
    }

В заключение, используя мой подход к UICollectionView, я показываю неправильные изображения в ячейку и дублирую их. Как мне предотвратить это?

1 Ответ

1 голос
/ 12 мая 2019

Вы спрашиваете:

Есть ли способ запросить несколько различных ресурсов параллельно, используя URLSession.shared.dataTask

По умолчанию он выполняет запросы параллельно.

Давайте вернемся на секунду: в своем предыдущем вопросе вы спрашивали, как реализовать расширение UIImageView, подобное Kingfisher. В моем ответе я упоминал об использовании objc_getAssociatedObject и objc_setAssociatedObject для достижения этой цели. Но в своем вопросе вы взяли логику связанного объекта и поместили ее в свой DataRequest объект.

Ваш мыслительный процесс - вытащить асинхронную логику поиска изображений из UIImageView - хорошая идея: вы можете запросить изображения для кнопок. Вы можете использовать обычную процедуру «получения изображения асинхронно», полностью отделенную от любых объектов UIKit. Поэтому абстрагирование кода сетевого уровня от расширения - отличная идея.

Но вся идея асинхронного поиска изображений UIImageView / UIButton заключается в том, что нам нужен элемент управления UIKit, где он может не только выполнять асинхронные запросы, но и, если ячейка с элементом управления используется повторно, он отменяет предыдущий асинхронный запрос (если есть) перед запуском следующего. Таким образом, если быстро прокрутить изображение до 80–99, запросы на ячейки с 0 по 79 будут отменены, и видимые изображения не будут отставать от всех этих старых запросов изображений.

Но для этого это означает, что элементу управления необходимо каким-то образом отслеживать предыдущий запрос для этой повторно используемой ячейки. А поскольку мы не можем добавлять сохраненные свойства в расширение UIImageView, поэтому мы используем шаблон objc_getAssociatedObject и objc_setAssociatedObject. Но это должно быть в представлении изображения.

К сожалению, в приведенном выше коде связанный объект находится в вашем DataRequest объекте. Во-первых, как я пытался обрисовать, вся идея в том, что представление изображения должно отслеживать предыдущий запрос для этого элемента управления. Помещение этого «отслеживания предыдущего запроса» в объекте DataRequest отрицает эту цель. Во-вторых, стоит отметить, что вам не нужны связанные объекты в ваших собственных типах, таких как DataRequest. У вас просто есть сохраненная собственность. Вам нужно пройти через эту глупость связанного объекта при расширении другого типа, такого как UIImageView.

Ниже приведен быстрый пример, демонстрирующий расширение UIImageView для асинхронного извлечения изображений. Обратите внимание, что здесь нет абстракции сетевого кода вне расширения, но обратите внимание, что логика связанного объекта для отслеживания предыдущего запроса должна оставаться с расширением.

private var taskKey: Void?

extension UIImageView {
    private static let imageProcessingQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".imageprocessing", attributes: .concurrent)

    private var savedTask: URLSessionTask? {
        get { return objc_getAssociatedObject(self, &taskKey) as? URLSessionTask }
        set { objc_setAssociatedObject(self, &taskKey, newValue, .OBJC_ASSOCIATION_RETAIN) }
    }

    /// Set image asynchronously.
    ///
    /// - Parameters:
    ///   - url: `URL` for image resource.
    ///   - placeholder: `UIImage` of placeholder image. If not supplied, `image` will be set to `nil` while request is underway.
    ///   - shouldResize: Whether the image should be scaled to the size of the image view. Defaults to `true`.

    func setImage(_ url: URL, placeholder: UIImage? = nil, shouldResize: Bool = true) {
        savedTask?.cancel()
        savedTask = nil

        image = placeholder
        if let image = ImageCache.shared[url] {
            DispatchQueue.main.async {
                UIView.transition(with: self, duration: 0.1, options: .transitionCrossDissolve, animations: {
                    self.image = image
                }, completion: nil)
            }
            return
        }

        var task: URLSessionTask!
        let size = bounds.size * UIScreen.main.scale
        task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
            guard
                error == nil,
                let httpResponse = response as? HTTPURLResponse,
                (200..<300) ~= httpResponse.statusCode,
                let data = data
            else {
                return
            }

            UIImageView.imageProcessingQueue.async { [weak self] in
                var image = UIImage(data: data)
                if shouldResize {
                    image = image?.scaledAspectFit(to: size)
                }

                ImageCache.shared[url] = image

                DispatchQueue.main.async {
                    guard
                        let self = self,
                        let savedTask = self.savedTask,
                        savedTask.taskIdentifier == task.taskIdentifier
                    else {
                        return
                    }
                    self.savedTask = nil

                    UIView.transition(with: self, duration: 0.1, options: .transitionCrossDissolve, animations: {
                        self.image = image
                    }, completion: nil)
                }
            }
        }
        task.resume()
        savedTask = task
    }
}

class ImageCache {
    static let shared = ImageCache()

    private let cache = NSCache<NSURL, UIImage>()
    private var observer: NSObjectProtocol?

    init() {
        observer = NotificationCenter.default.addObserver(forName: UIApplication.didReceiveMemoryWarningNotification, object: nil, queue: nil) { [weak self] _ in
            self?.cache.removeAllObjects()
        }
    }

    deinit {
        NotificationCenter.default.removeObserver(observer!)
    }

    subscript(url: URL) -> UIImage? {
        get {
            return cache.object(forKey: url as NSURL)
        }

        set {
            if let data = newValue {
                cache.setObject(data, forKey: url as NSURL)
            } else {
                cache.removeObject(forKey: url as NSURL)
            }
        }
    }
}

И это моя процедура изменения размера:

extension UIImage {

    /// Resize the image to be the required size, stretching it as needed.
    ///
    /// - parameter newSize:      The new size of the image.
    /// - parameter contentMode:  The `UIView.ContentMode` to be applied when resizing image.
    ///                           Either `.scaleToFill`, `.scaleAspectFill`, or `.scaleAspectFit`.
    ///
    /// - returns:                Return `UIImage` of resized image.

    func scaled(to newSize: CGSize, contentMode: UIView.ContentMode = .scaleToFill) -> UIImage? {
        switch contentMode {
        case .scaleToFill:
            return filled(to: newSize)

        case .scaleAspectFill, .scaleAspectFit:
            let horizontalRatio = size.width  / newSize.width
            let verticalRatio   = size.height / newSize.height

            let ratio: CGFloat!
            if contentMode == .scaleAspectFill {
                ratio = min(horizontalRatio, verticalRatio)
            } else {
                ratio = max(horizontalRatio, verticalRatio)
            }

            let sizeForAspectScale = CGSize(width: size.width / ratio, height: size.height / ratio)
            let image = filled(to: sizeForAspectScale)
            let doesAspectFitNeedCropping = contentMode == .scaleAspectFit && (newSize.width > sizeForAspectScale.width || newSize.height > sizeForAspectScale.height)
            if contentMode == .scaleAspectFill || doesAspectFitNeedCropping {
                let subRect = CGRect(
                    x: floor((sizeForAspectScale.width - newSize.width) / 2.0),
                    y: floor((sizeForAspectScale.height - newSize.height) / 2.0),
                    width: newSize.width,
                    height: newSize.height)
                return image?.cropped(to: subRect)
            }
            return image

        default:
            return nil
        }
    }

    /// Resize the image to be the required size, stretching it as needed.
    ///
    /// - parameter newSize:   The new size of the image.
    ///
    /// - returns:             Resized `UIImage` of resized image.

    func filled(to newSize: CGSize) -> UIImage? {
        let format = UIGraphicsImageRendererFormat()
        format.opaque = false
        format.scale = scale

        return UIGraphicsImageRenderer(size: newSize, format: format).image { _ in
            draw(in: CGRect(origin: .zero, size: newSize))
        }
    }

    /// Crop the image to be the required size.
    ///
    /// - parameter bounds:    The bounds to which the new image should be cropped.
    ///
    /// - returns:             Cropped `UIImage`.

    func cropped(to bounds: CGRect) -> UIImage? {
        // if bounds is entirely within image, do simple CGImage `cropping` ...

        if CGRect(origin: .zero, size: size).contains(bounds) {
            return cgImage?.cropping(to: bounds * scale).flatMap {
                UIImage(cgImage: $0, scale: scale, orientation: imageOrientation)
            }
        }

        // ... otherwise, manually render whole image, only drawing what we need

        let format = UIGraphicsImageRendererFormat()
        format.opaque = false
        format.scale = scale

        return UIGraphicsImageRenderer(size: bounds.size, format: format).image { _ in
            let origin = CGPoint(x: -bounds.minX, y: -bounds.minY)
            draw(in: CGRect(origin: origin, size: size))
        }
    }

    /// Resize the image to fill the rectange of the specified size, preserving the aspect ratio, trimming if needed.
    ///
    /// - parameter newSize:   The new size of the image.
    ///
    /// - returns:             Return `UIImage` of resized image.

    func scaledAspectFill(to newSize: CGSize) -> UIImage? {
        return scaled(to: newSize, contentMode: .scaleAspectFill)
    }

    /// Resize the image to fit within the required size, preserving the aspect ratio, with no trimming taking place.
    ///
    /// - parameter newSize:   The new size of the image.
    ///
    /// - returns:             Return `UIImage` of resized image.

    func scaledAspectFit(to newSize: CGSize) -> UIImage? {
        return scaled(to: newSize, contentMode: .scaleAspectFit)
    }

    /// Create smaller image from `Data`
    ///
    /// - Parameters:
    ///   - data: The image `Data`.
    ///   - maxSize: The maximum edge size.
    ///   - scale: The scale of the image (defaults to device scale if 0 or omitted.
    /// - Returns: The scaled `UIImage`.

    class func thumbnail(from data: Data, maxSize: CGFloat, scale: CGFloat = 0) -> UIImage? {
        guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else {
            return nil
        }

        return thumbnail(from: imageSource, maxSize: maxSize, scale: scale)
    }

    /// Create smaller image from `URL`
    ///
    /// - Parameters:
    ///   - data: The image file URL.
    ///   - maxSize: The maximum edge size.
    ///   - scale: The scale of the image (defaults to device scale if 0 or omitted.
    /// - Returns: The scaled `UIImage`.

    class func thumbnail(from fileURL: URL, maxSize: CGFloat, scale: CGFloat = 0) -> UIImage? {
        guard let imageSource = CGImageSourceCreateWithURL(fileURL as CFURL, nil) else {
            return nil
        }

        return thumbnail(from: imageSource, maxSize: maxSize, scale: scale)
    }

    private class func thumbnail(from imageSource: CGImageSource, maxSize: CGFloat, scale: CGFloat) -> UIImage? {
        let scale = scale == 0 ? UIScreen.main.scale : scale
        let options: [NSString: Any] = [
            kCGImageSourceThumbnailMaxPixelSize: maxSize * scale,
            kCGImageSourceCreateThumbnailFromImageAlways: true
        ]
        if let scaledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) {
            return UIImage(cgImage: scaledImage, scale: scale, orientation: .up)
        }
        return nil
    }

}

extension CGSize {
    static func * (lhs: CGSize, rhs: CGFloat) -> CGSize {
        return CGSize(width: lhs.width * rhs, height: lhs.height * rhs)
    }
}

extension CGPoint {
    static func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
        return CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
    }
}

extension CGRect {
    static func * (lhs: CGRect, rhs: CGFloat) -> CGRect {
        return CGRect(origin: lhs.origin * rhs, size: lhs.size * rhs)
    }
}

С учетом вышесказанного нам действительно следует ограничить наши одновременные запросы чем-то разумным (4-6 за раз), чтобы они не пытались запускаться до тех пор, пока не будут выполнены (или отменены) предыдущие запросы, чтобы избежать тайм-аутов. Типичное решение - обернуть запросы асинхронными подклассами Operation, добавить их в очередь операций и ограничить maxConcurrentOperationCount любым значением, которое вы выберете.

...