Ios 是否有一种方法可以使用URLSession.shared.dataTask并行请求多个不同的资源
我在这里找到了一段关于如何在没有任何中断的情况下同时下载图像的代码Ios 是否有一种方法可以使用URLSession.shared.dataTask并行请求多个不同的资源,ios,swift,urlsession,nsurlsessiondatatask,Ios,Swift,Urlsession,Nsurlsessiondatatask,我在这里找到了一段关于如何在没有任何中断的情况下同时下载图像的代码 func loadImageRobsAnswer(with urlString: String?) { // cancel prior task, if any weak var oldTask = currentTask currentTask = nil oldTask?.cancel() // reset imageview's image self.im
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上使用我的方法是将错误的图像显示到单元格中并进行复制,如何防止这种情况发生?您会问:
有没有一种方法可以使用URLSession.shared.dataTask
默认情况下,它会并行执行请求
让我们后退一步:在前面的问题中,您询问如何实现类似翠鸟的UIImageView
扩展。在中,我提到了使用objc\u getAssociatedObject
和objc\u setAssociatedObject
来实现这一点。但在这里的问题中,您已经获取了相关的对象逻辑,并将其放入DataRequest
对象中
从UIImageView
中提取异步图像检索逻辑是一个好主意:您可能需要为按钮请求图像。您可能需要一个通用的“异步获取图像”例程,该例程完全独立于任何UIKit对象。因此,从扩展中抽象出网络层代码是一个很好的想法
但是,异步图像检索UIImageView
/UIButton
扩展背后的整个思想是,我们需要一个UIKit控件,它不仅可以执行异步请求,而且如果使用该控件的单元格被重用,它将在启动下一个异步请求之前取消先前的异步请求(如果有)。这样,如果我们快速向下滚动到图像80到99,对单元格0到79的请求将被取消,可见图像不会在所有这些旧图像请求之后被积压
但要实现这一点,这意味着控件需要某种方式来跟踪之前对重用单元的请求。因为我们不能在UIImageView
扩展中添加存储属性,所以我们使用objc\u getAssociatedObject
和objc\u 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)
}
}
}
}
话虽如此,我们确实应该将并发请求限制在合理的范围内(一次4-6个),以便在之前的请求完成(或取消)之前,它们不会尝试启动,以避免超时。典型的解决方案是使用异步
操作
子类包装请求,将它们添加到操作队列中,并将maxConcurrentOperationCount
约束为您选择的任何值。但URLSession将自动保持并发请求的合理性。@Rob是否始终必须将映像下载任务放在UIImageView扩展中,如果我想将setImage函数提取到一个具有不同的完成处理程序(数据类型为Json或字符串)的类中,并且仍然避免请求和响应分散在各个类中,该怎么办。@matt是的,但它的上限是一个相当低的数字,并且在边缘情况下,您可以开始达到后一个请求的超时限制。我认为请求队列比增加最大计数和/或超时要好。我正在研究如何用异步操作子类包装我的请求,正如你所建议的那样。@LeoDabus哦,我明白你的意思了。但是,如果aspect适合,我个人不想使用原始图像的format.opaque
。
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)
}
}