Swift: 在 UITableViewCell 中异步加载图像
Swift: load images Async in UITableViewCell
我有一个 tableview
是我用代码创建的(没有 storyboard
):
class MSContentVerticalList: MSContent,UITableViewDelegate,UITableViewDataSource {
var tblView:UITableView!
var dataSource:[MSC_VCItem]=[]
init(Frame: CGRect,DataSource:[MSC_VCItem]) {
super.init(frame: Frame)
self.dataSource = DataSource
tblView = UITableView(frame: Frame, style: .Plain)
tblView.delegate = self
tblView.dataSource = self
self.addSubview(tblView)
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .Subtitle, reuseIdentifier: nil)
let record = dataSource[indexPath.row]
cell.textLabel!.text = record.Title
cell.imageView!.downloadFrom(link: record.Icon, contentMode: UIViewContentMode.ScaleAspectFit)
cell.imageView!.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
print(cell.imageView!.frame)
cell.detailTextLabel!.text = record.SubTitle
return cell
}
}
在其他 class 中,我有一个异步下载图像的扩展方法:
extension UIImageView
{
func downloadFrom(link link:String?, contentMode mode: UIViewContentMode)
{
contentMode = mode
if link == nil
{
self.image = UIImage(named: "default")
return
}
if let url = NSURL(string: link!)
{
print("\nstart download: \(url.lastPathComponent!)")
NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { (data, _, error) -> Void in
guard let data = data where error == nil else {
print("\nerror on download \(error)")
return
}
dispatch_async(dispatch_get_main_queue()) { () -> Void in
print("\ndownload completed \(url.lastPathComponent!)")
self.image = UIImage(data: data)
}
}).resume()
}
else
{
self.image = UIImage(named: "default")
}
}
}
我在其他地方使用了这个功能并且工作正常,根据我的日志我了解到图像下载没有问题(当单元格被渲染时)并且在图像下载后,单元格 UI 没有更新。
我也尝试过使用像 Haneke 这样的缓存库,但问题仍然存在并且没有改变。
请帮我理解错误
谢谢
设置图片后你应该调用self.layoutSubviews()
编辑:从 setNeedsLayout
更正为 layoutSubviews
通过子类化 UITableViewCell 创建您自己的单元格。您正在使用的样式 .Subtitle 没有图像视图,即使 属性 可用。只有样式 UITableViewCellStyleDefault 具有图像视图。
问题是 UITableViewCell
的 .subtitle
再现将在 cellForRowAtIndexPath
returns 后立即布局单元格(覆盖您尝试设置 frame
图像视图)。因此,如果您异步检索图像,单元格将 re-laid 输出,就好像没有图像可显示一样(因为您没有将图像视图的 image
属性 初始化为任何东西),并且当您稍后异步更新 imageView
时,单元格的布局方式已经使您无法看到下载的图像。
这里有几个解决方案:
您可以让 download
将图像更新为 default
,不仅是在没有 URL 时,还有 URL ](因此您首先将其设置为默认图像,然后将图像更新为您从网络下载的图像):
extension UIImageView {
func download(from url: URL, contentMode mode: UIView.ContentMode = .scaleAspectFill, placeholder: UIImage? = nil) {
contentMode = mode
image = placeholder
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, let response = response as? HTTPURLResponse, error == nil else {
print("error on download \(error ?? URLError(.badServerResponse))")
return
}
guard 200 ..< 300 ~= response.statusCode else {
print("statusCode != 2xx; \(response.statusCode)")
return
}
guard let image = UIImage(data: data) else {
print("not valid image")
return
}
DispatchQueue.main.async {
print("download completed \(url.lastPathComponent)")
self.image = image
}
}.resume()
}
}
这确保单元格将根据图像的存在进行布局,无论如何,图像视图的异步更新将起作用(有点:见下文)。
而不是使用 UITableViewCell
的动态布局 .subtitle
再现,您还可以创建自己的单元格原型,该原型适当地布局并具有固定的图像大小看法。这样,如果没有立即可用的图像,它不会像没有可用图像一样重新格式化单元格。这使您可以使用自动布局完全控制单元格的格式。
您还可以定义 downloadFrom
方法以获取额外的第三个参数,即下载完成时您将调用的闭包。然后你可以在那个闭包里面做一个 reloadRowsAtIndexPaths
。不过,这假定您修复此代码以缓存下载的图像(例如 NSCache
),以便您可以在再次下载之前检查是否有缓存的图像。
话虽如此,正如我上面提到的,这个基本模式存在一些问题:
- 如果您向下滚动然后向上滚动,您将re-retrieve 来自网络的图像。您确实希望在再次检索之前缓存以前下载的图像。
理想情况下,您的服务器响应 headers 已正确配置,以便内置 NSURLCache
会为您处理此问题,但您必须对其进行测试。或者,您可以自己将图像缓存在自己的 NSCache
.
中
如果您快速向下滚动到第 100 行,您真的不希望可见单元格积压在前 99 行不再可见的图像请求之后。您确实想取消对滚动到屏幕外的单元格的请求。 (或者使用dequeueCellForRowAtIndexPath
,你re-use个单元格,然后你可以写代码取消之前的请求。)
如上所述,你真的很想做dequeueCellForRowAtIndexPath
这样你就不必不必要地实例化UITableViewCell
objects。你应该重复使用它们。
就我个人而言,我可能建议您 (a) 使用 dequeueCellForRowAtIndexPath
,然后 (b) 将其与成熟的 UIImageViewCell
类别之一结合使用,例如 AlamofireImage, SDWebImage, DFImageManager or Kingfisher。执行必要的缓存和取消先前的请求是一项 non-trivial 练习,使用其中一个 UIImageView
扩展将简化您的生活。如果您决定自己执行此操作,您可能还想查看这些扩展的一些代码,这样您就可以 pick-up 了解如何正确执行此操作。
--
例如,使用AlamofireImage,您可以:
定义自定义 table 视图单元格子class:
class CustomCell : UITableViewCell {
@IBOutlet weak var customImageView: UIImageView!
@IBOutlet weak var customTitleLabel: UILabel!
@IBOutlet weak var customSubtitleLabel: UILabel!
}
将单元格原型添加到您的 table 视图故事板,指定 (a) CustomCell
的基础 class; (b) CustomCell
的故事板 ID; (c) 将图像视图和两个标签添加到您的单元格原型,将 @IBOutlets
连接到您的 CustomCell
subclass; (d) 添加任何必要的约束来定义图像视图的 placement/size 和两个标签。
您可以使用自动布局约束来定义图像视图的尺寸
你的 cellForRowAtIndexPath
,然后可以做类似的事情:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
let record = dataSource[indexPath.row]
cell.customTitleLabel.text = record.title
cell.customSubtitleLabel.text = record.subtitle
if let url = record.url {
cell.customImageView.af.setImage(withURL: url)
}
return cell
}
有了它,您不仅可以享受基本的异步图像更新,还可以享受图像缓存、可见图像的优先级排序,因为我们正在重用出队的单元格,它更高效,等等。通过使用带有约束的单元格原型和您的custom table view cell subclass,一切都正确布局,让您无需在代码中手动调整 frame
。
无论您使用这些 UIImageView
扩展中的哪一个,过程基本相同,但目标是让您摆脱自己编写扩展的困境。
天哪,layoutSubviews不建议直接使用
解决问题的正确方法是调用:
[self setNeedsLayout];
[self layoutIfNeeded];
在这里,这两种方式必须一起调用。
试试这个,祝你好运。
这里更喜欢 SDWebImages 库 link
它将异步下载图片并缓存图片
也很容易集成到项目中
我有一个 tableview
是我用代码创建的(没有 storyboard
):
class MSContentVerticalList: MSContent,UITableViewDelegate,UITableViewDataSource {
var tblView:UITableView!
var dataSource:[MSC_VCItem]=[]
init(Frame: CGRect,DataSource:[MSC_VCItem]) {
super.init(frame: Frame)
self.dataSource = DataSource
tblView = UITableView(frame: Frame, style: .Plain)
tblView.delegate = self
tblView.dataSource = self
self.addSubview(tblView)
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .Subtitle, reuseIdentifier: nil)
let record = dataSource[indexPath.row]
cell.textLabel!.text = record.Title
cell.imageView!.downloadFrom(link: record.Icon, contentMode: UIViewContentMode.ScaleAspectFit)
cell.imageView!.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
print(cell.imageView!.frame)
cell.detailTextLabel!.text = record.SubTitle
return cell
}
}
在其他 class 中,我有一个异步下载图像的扩展方法:
extension UIImageView
{
func downloadFrom(link link:String?, contentMode mode: UIViewContentMode)
{
contentMode = mode
if link == nil
{
self.image = UIImage(named: "default")
return
}
if let url = NSURL(string: link!)
{
print("\nstart download: \(url.lastPathComponent!)")
NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { (data, _, error) -> Void in
guard let data = data where error == nil else {
print("\nerror on download \(error)")
return
}
dispatch_async(dispatch_get_main_queue()) { () -> Void in
print("\ndownload completed \(url.lastPathComponent!)")
self.image = UIImage(data: data)
}
}).resume()
}
else
{
self.image = UIImage(named: "default")
}
}
}
我在其他地方使用了这个功能并且工作正常,根据我的日志我了解到图像下载没有问题(当单元格被渲染时)并且在图像下载后,单元格 UI 没有更新。
我也尝试过使用像 Haneke 这样的缓存库,但问题仍然存在并且没有改变。
请帮我理解错误
谢谢
设置图片后你应该调用self.layoutSubviews()
编辑:从 setNeedsLayout
更正为 layoutSubviews
通过子类化 UITableViewCell 创建您自己的单元格。您正在使用的样式 .Subtitle 没有图像视图,即使 属性 可用。只有样式 UITableViewCellStyleDefault 具有图像视图。
问题是 UITableViewCell
的 .subtitle
再现将在 cellForRowAtIndexPath
returns 后立即布局单元格(覆盖您尝试设置 frame
图像视图)。因此,如果您异步检索图像,单元格将 re-laid 输出,就好像没有图像可显示一样(因为您没有将图像视图的 image
属性 初始化为任何东西),并且当您稍后异步更新 imageView
时,单元格的布局方式已经使您无法看到下载的图像。
这里有几个解决方案:
您可以让
download
将图像更新为default
,不仅是在没有 URL 时,还有 URL ](因此您首先将其设置为默认图像,然后将图像更新为您从网络下载的图像):extension UIImageView { func download(from url: URL, contentMode mode: UIView.ContentMode = .scaleAspectFill, placeholder: UIImage? = nil) { contentMode = mode image = placeholder URLSession.shared.dataTask(with: url) { data, response, error in guard let data = data, let response = response as? HTTPURLResponse, error == nil else { print("error on download \(error ?? URLError(.badServerResponse))") return } guard 200 ..< 300 ~= response.statusCode else { print("statusCode != 2xx; \(response.statusCode)") return } guard let image = UIImage(data: data) else { print("not valid image") return } DispatchQueue.main.async { print("download completed \(url.lastPathComponent)") self.image = image } }.resume() } }
这确保单元格将根据图像的存在进行布局,无论如何,图像视图的异步更新将起作用(有点:见下文)。
而不是使用
UITableViewCell
的动态布局.subtitle
再现,您还可以创建自己的单元格原型,该原型适当地布局并具有固定的图像大小看法。这样,如果没有立即可用的图像,它不会像没有可用图像一样重新格式化单元格。这使您可以使用自动布局完全控制单元格的格式。您还可以定义
downloadFrom
方法以获取额外的第三个参数,即下载完成时您将调用的闭包。然后你可以在那个闭包里面做一个reloadRowsAtIndexPaths
。不过,这假定您修复此代码以缓存下载的图像(例如NSCache
),以便您可以在再次下载之前检查是否有缓存的图像。
话虽如此,正如我上面提到的,这个基本模式存在一些问题:
- 如果您向下滚动然后向上滚动,您将re-retrieve 来自网络的图像。您确实希望在再次检索之前缓存以前下载的图像。
理想情况下,您的服务器响应 headers 已正确配置,以便内置 NSURLCache
会为您处理此问题,但您必须对其进行测试。或者,您可以自己将图像缓存在自己的 NSCache
.
如果您快速向下滚动到第 100 行,您真的不希望可见单元格积压在前 99 行不再可见的图像请求之后。您确实想取消对滚动到屏幕外的单元格的请求。 (或者使用
dequeueCellForRowAtIndexPath
,你re-use个单元格,然后你可以写代码取消之前的请求。)如上所述,你真的很想做
dequeueCellForRowAtIndexPath
这样你就不必不必要地实例化UITableViewCell
objects。你应该重复使用它们。
就我个人而言,我可能建议您 (a) 使用 dequeueCellForRowAtIndexPath
,然后 (b) 将其与成熟的 UIImageViewCell
类别之一结合使用,例如 AlamofireImage, SDWebImage, DFImageManager or Kingfisher。执行必要的缓存和取消先前的请求是一项 non-trivial 练习,使用其中一个 UIImageView
扩展将简化您的生活。如果您决定自己执行此操作,您可能还想查看这些扩展的一些代码,这样您就可以 pick-up 了解如何正确执行此操作。
--
例如,使用AlamofireImage,您可以:
定义自定义 table 视图单元格子class:
class CustomCell : UITableViewCell { @IBOutlet weak var customImageView: UIImageView! @IBOutlet weak var customTitleLabel: UILabel! @IBOutlet weak var customSubtitleLabel: UILabel! }
将单元格原型添加到您的 table 视图故事板,指定 (a)
CustomCell
的基础 class; (b)CustomCell
的故事板 ID; (c) 将图像视图和两个标签添加到您的单元格原型,将@IBOutlets
连接到您的CustomCell
subclass; (d) 添加任何必要的约束来定义图像视图的 placement/size 和两个标签。您可以使用自动布局约束来定义图像视图的尺寸
你的
cellForRowAtIndexPath
,然后可以做类似的事情:func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell let record = dataSource[indexPath.row] cell.customTitleLabel.text = record.title cell.customSubtitleLabel.text = record.subtitle if let url = record.url { cell.customImageView.af.setImage(withURL: url) } return cell }
有了它,您不仅可以享受基本的异步图像更新,还可以享受图像缓存、可见图像的优先级排序,因为我们正在重用出队的单元格,它更高效,等等。通过使用带有约束的单元格原型和您的custom table view cell subclass,一切都正确布局,让您无需在代码中手动调整
frame
。
无论您使用这些 UIImageView
扩展中的哪一个,过程基本相同,但目标是让您摆脱自己编写扩展的困境。
天哪,layoutSubviews不建议直接使用
解决问题的正确方法是调用:
[self setNeedsLayout];
[self layoutIfNeeded];
在这里,这两种方式必须一起调用。
试试这个,祝你好运。
这里更喜欢 SDWebImages 库 link
它将异步下载图片并缓存图片 也很容易集成到项目中