Вы спрашиваете:
Есть ли способ запросить несколько различных ресурсов параллельно, используя 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
любым значением, которое вы выберете.