Swift异步加载图片

37

我正在处理异步显示图片的问题。我尝试创建一个新的线程来下载图片,然后在主线程上刷新。

func asyncLoadImg(product:Product,imageView:UIImageView){
    let downloadQueue = dispatch_queue_create("com.myApp.processdownload", nil)
    dispatch_async(downloadQueue){
        let data = NSData(contentsOfURL: NSURL(string: product.productImage)!)
        var image:UIImage?
        if data != nil{
            image = UIImage(data: data!)
        }
        dispatch_async(dispatch_get_main_queue()){
            imageView.image = image
        }

    }
}

当我尝试调试时,当它到达dispatch_async(downloadQueue)时,它跳出了函数。有任何建议吗?谢谢


https://dev59.com/PmAf5IYBdhLWcg3w0Vi7#27712427 - Leo Dabus
:( 不起作用,出现相同的错误。后台线程中的操作从未被执行。 - Janice Zhan
@LeoDabus,你能更具体地解释一下吗?我不理解。 - Janice Zhan
将该UIImageView扩展添加到您的项目中,然后只需使用yourImageView.downloadedFrom(yourImageLink, ... )加载您的图像。 - Leo Dabus
1
将调试点放在代码块内部,单击“步入”按钮无法进入该代码块。 - RJE
@JaniceZhan 请将准确的答案标记为正确答案,这样可以使其他人受益并解决问题,从而获得最大的赞同。 - Shobhakar Tiwari
8个回答

106

**Swift 5.0+ 更新的代码:

extension UIImageView {

    
        func imageFromServerURL(_ URLString: String, placeHolder: UIImage?) {

        self.image = nil
        //If imageurl's imagename has space then this line going to work for this
        let imageServerUrl = URLString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
        

        if let url = URL(string: imageServerUrl) {
            URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in

                //print("RESPONSE FROM API: \(response)")
                if error != nil {
                    print("ERROR LOADING IMAGES FROM URL: \(error)")
                    DispatchQueue.main.async {
                        self.image = placeHolder
                    }
                    return
                }
                DispatchQueue.main.async {
                    if let data = data {
                        if let downloadedImage = UIImage(data: data) {
                       
                            self.image = downloadedImage
                        }
                    }
                }
            }).resume()
        }
    }
}

现在,您只需执行以下操作即可从服务器 URL 加载图像:

  1. 使用 Swift 5.0+ 更新的代码和占位符图像: UIImageView.imageFromServerURL(URLString:“此处是服务器 URL”,placeHolder:以 UIImage 格式的占位符图像)

简单!


2
我认为这应该是正确的答案,因为它更加简洁。:+1: - Andrew Sowers
2
@Kirill 在 Swift 3 中更新了代码。试一试吧。希望能解决你的问题。 - Shobhakar Tiwari
1
优秀的代码。这是解决这个常见问题最好/最简单的方法。感谢@ShobhakarTiwari! - Waggoner_Keith
1
我正在使用这段代码在TableView中的水平CollectionView中显示图像,但是当滚动TableView单元格时,图像会在ImageView上产生冲突。如何解决? - Dharini
1
你的Swift 5更新无法编译。 let imageCache = NSCache<NSString, UIImage>()报错: 扩展不应包含存储属性 - Trevor
显示剩余12条评论

30

在Swift3中使用extension。为了解决网络问题,我建议您使用NSCache

import UIKit

let imageCache = NSCache<NSString, AnyObject>()

extension UIImageView {
    func loadImageUsingCache(withUrl urlString : String) {
        let url = URL(string: urlString)
        self.image = nil

        // check cached image
        if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
            self.image = cachedImage
            return
        }

        // if not, download image from url
        URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
            if error != nil {
                print(error!)
                return
            }

            DispatchQueue.main.async {
                if let image = UIImage(data: data!) {
                    imageCache.setObject(image, forKey: urlString as NSString)
                    self.image = image
                }
            }

        }).resume()
    }
}

希望它能帮到你!


1
为什么你使用了 NSStringAnyObject,而不是 StringAny - Jack

5

继续Shobhakar Tiwari的回答,我认为在这种情况下,有一个默认图片以防错误和加载目的是很有帮助的,因此我更新了它,以包括可选的默认图片:

Swift 3

extension UIImageView {
public func imageFromServerURL(urlString: String, defaultImage : String?) {
    if let di = defaultImage {
        self.image = UIImage(named: di)
    }

    URLSession.shared.dataTask(with: NSURL(string: urlString)! as URL, completionHandler: { (data, response, error) -> Void in

        if error != nil {
            print(error ?? "error")
            return
        }
        DispatchQueue.main.async(execute: { () -> Void in
            let image = UIImage(data: data!)
            self.image = image
        })

    }).resume()
  }
}

4
请不要重复回答,你可以在答案下方发表评论,以便开发者从中受益。 - Shobhakar Tiwari

2

这个解决方案可以使滚动更快,而不会进行不必要的图像更新。您需要将url属性添加到我们的单元格类中:

class OfferItemCell: UITableViewCell {
    @IBOutlet weak var itemImageView: UIImageView!
    @IBOutlet weak var titleLabel: UILabel! 
    var imageUrl: String?
}

并添加扩展:

import Foundation
import UIKit

let imageCache = NSCache<AnyObject, AnyObject>()
let imageDownloadUtil: ImageDownloadUtil = ImageDownloadUtil()

extension OfferItemCell {
    func loadImageUsingCacheWithUrl(urlString: String  ) {
         self.itemImageView.image = nil
        if let cachedImage = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
            self.itemImageView.image = cachedImage
            return
        }
        DispatchQueue.global(qos: .background).async {
            imageDownloadUtil.getImage(url: urlString, completion: {
                image in
                DispatchQueue.main.async {
                    if self.imageUrl == urlString{
                        imageCache.setObject(image, forKey: urlString as AnyObject)
                     self.itemImageView.image = image
                    }
                }
            })
        }
    }
}

你还可以改进它并将一些代码提取到更通用的单元格类中,例如CustomCellWithImage,以使其更具可重用性。


1
ImageDownloadUtil出现编译错误。我看不到它是如何声明的,也不确定如何制作自己的版本。 - iCyberPaul

1

其中最好的方法之一是使用SDWebImage

Swift示例:

import SDWebImage
imageView.sd_setImage(with: URL(string: "ImageUrl"), placeholderImage: UIImage(named: "placeholder.png"))

Objective C 示例:

#import <SDWebImage/UIImageView+WebCache.h>
[imageView sd_setImageWithURL:[NSURL URLWithString:@"ImageUrl"]
         placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

1
这里有一段代码,可能会对你有帮助。
   let cacheKey = indexPath.row
   if(self.imageCache?.objectForKey(cacheKey) != nil){
           cell.img.image = self.imageCache?.objectForKey(cacheKey) as? UIImage
    }else{
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), {
             if let url = NSURL(string: imgUrl) {
                  if let data = NSData(contentsOfURL: url) {
                            let image: UIImage = UIImage(data: data)!
                            self.imageCache?.setObject(image, forKey: cacheKey)
                            dispatch_async(dispatch_get_main_queue(), {
                                cell.img.image = image
                            })
                        }
                    }
                })
            }

使用此图像将下载并缓存,而不会使表格视图滚动出现延迟。

很棒的解决方案!但是如果我快速滚动,仍然可以看到图像从一个变成另一个,有没有办法避免这种情况或者这是不可能的? - Adrian
@Adrian 是的,你可以检查图片是否为nil,如果是nil就加载它,否则不加载。 - wm.p1us

1

在 Swift 4 中最常见的一种不会出现闪烁或更改图片效果的异步加载图片的方式是使用自定义的 UIImageView 类,像这样:

//MARK: - 'asyncImagesCashArray' is a global varible cashed UIImage
var asyncImagesCashArray = NSCache<NSString, UIImage>()

class AyncImageView: UIImageView {

//MARK: - Variables
private var currentURL: NSString?

//MARK: - Public Methods

func loadAsyncFrom(url: String, placeholder: UIImage?) {
    let imageURL = url as NSString
    if let cashedImage = asyncImagesCashArray.object(forKey: imageURL) {
        image = cashedImage
        return
    }
    image = placeholder
    currentURL = imageURL
    guard let requestURL = URL(string: url) else { image = placeholder; return }
    URLSession.shared.dataTask(with: requestURL) { (data, response, error) in
        DispatchQueue.main.async { [weak self] in
            if error == nil {
                if let imageData = data {
                    if self?.currentURL == imageURL {
                        if let imageToPresent = UIImage(data: imageData) {
                            asyncImagesCashArray.setObject(imageToPresent, forKey: imageURL)
                            self?.image = imageToPresent
                        } else {
                            self?.image = placeholder
                        }
                    }
                } else {
                    self?.image = placeholder
                }
            } else {
                self?.image = placeholder
            }
        }
    }.resume()
}
}

以下是在UITableViewCell中使用此类的示例:

class CatCell: UITableViewCell {

//MARK: - Outlets
@IBOutlet weak var catImageView: AyncImageView!

//MARK: - Variables

var urlString: String? {
    didSet {
        if let url = urlString {
            catImageView.loadAsyncFrom(url: url, placeholder: nil)
        }
    }
}

override func awakeFromNib() {
    super.awakeFromNib()
}
}

0
从Swift 5.5开始,只需调用一个函数就可以了,就像这样:
imageView.setImage(url: myImageURL, placeholder: thePlaceholder)

使用这个简单的扩展:
extension UIImageView {
    func setImage(url: URL, placeholder: UIImage?) {
        self.image = placeholder

        Task { [weak self] in
            let (data, _) = try await URLSession.shared.data(from: url)
            self?.image = UIImage(data: data)
        }
    }
}

请注意,直到图像从网络上到达之前,将使用占位符。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接