12

我有一个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
}
}

在其他类中,我有一个用于异步下载图像的扩展方法:

   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这样的缓存库,但问题存在并且没有改变。

请帮助我理解错误

谢谢

4

5 回答 5

11

设置图像后,您应该调用self.layoutSubviews()

编辑:更正setNeedsLayoutlayoutSubviews

于 2015-11-03T12:58:08.557 回答
10

The issue is that the .Subtitle rendition of UITableViewCell will layout the cell as soon as cellForRowAtIndexPath returns (overriding your attempt to set the frame of the image view). Thus, if you are asynchronously retrieving the image, the cell will be re-laid out as if there was no image to show (because you're not initializing the image view's image property to anything), and when you update the imageView asynchronously later, the cell will have already be laid out in a manner such that you won't be able to see the image you downloaded.

There are a couple of solutions here:

  1. You can have the downloadFrom update the image to default not only when there is no URL, but also when there is a URL (so you'll first set it to the default image, and later update the image to the one that you downloaded from the network):

    extension UIImageView {
        func downloadFrom(link link:String?, contentMode mode: UIViewContentMode) {
            contentMode = mode
            image = UIImage(named: "default")
            if link != nil, let url = NSURL(string: link!) {
                NSURLSession.sharedSession().dataTaskWithURL(url) { data, response, error in
                    guard let data = data where error == nil else {
                        print("\nerror on download \(error)")
                        return
                    }
                    if let httpResponse = response as? NSHTTPURLResponse where httpResponse.statusCode != 200 {
                        print("statusCode != 200; \(httpResponse.statusCode)")
                        return
                    }
                    dispatch_async(dispatch_get_main_queue()) {
                        print("\ndownload completed \(url.lastPathComponent!)")
                        self.image = UIImage(data: data)
                    }
                    }.resume()
            } else {
                self.image = UIImage(named: "default")
            }
        }
    }
    

    This ensures that the cell will be laid out for the presence of an image, regardless, and thus the asynchronous updating of the image view will work (sort of: see below).

  2. Rather than using the dynamically laid out .Subtitle rendition of UITableViewCell, you can also create your own cell prototype which is laid out appropriately with a fixed size for the image view. That way, if there is no image immediately available, it won't reformat the cell as if there was no image available. This gives you complete control over the formatting of the cell using autolayout.

  3. You can also define your downloadFrom method to take an additional third parameter, a closure that you'll call when the download is done. Then you can do a reloadRowsAtIndexPaths inside that closure. This assumes, though, that you fix this code to cache downloaded images (in a NSCache for example), so that you can check to see if you have a cached image before downloading again.

Having said that, as I alluded to above, there are some problems with this basic pattern:

  1. If you scroll down and then scroll back up, you are going to re-retrieve the image from the network. You really want to cache the previously downloaded images before retrieving them again.

    Ideally, your server's response headers are configured properly so that the built in NSURLCache will take care of this for you, but you'd have to test that. Alternatively, you might cache the images yourself in your own NSCache.

  2. If you scroll down quickly to, say, the 100th row, you really don't want the visible cells backlogged behind image requests for the first 99 rows that are no longer visible. You really want to cancel requests for cells that scroll off screen. (Or use dequeueCellForRowAtIndexPath, where you re-use cells, and then you can write code to cancel the previous request.)

  3. As mentioned above, you really want to do dequeueCellForRowAtIndexPath so that you don't have to unnecessarily instantiate UITableViewCell objects. You should be reusing them.

Personally, I might suggest that you (a) use dequeueCellForRowAtIndexPath, and then (b) marry this with one of the well established UIImageViewCell categories such as AlamofireImage, SDWebImage, DFImageManager or Kingfisher. To do the necessary caching and cancelation of prior requests is a non-trivial exercise, and using one of those UIImageView extensions will simplify your life. And if you're determined to do this yourself, you might want to still look at some of the code for those extensions, so you can pick-up ideas on how to do this properly.

--

For example, using AlamofireImage, you can:

  1. Define a custom table view cell subclass:

    class CustomCell : UITableViewCell {
        @IBOutlet weak var customImageView: UIImageView!
        @IBOutlet weak var customTitleLabel: UILabel!
        @IBOutlet weak var customSubtitleLabel: UILabel!
    }
    
  2. Add a cell prototype to your table view storyboard, specifying (a) a base class of CustomCell; (b) a storyboard id of CustomCell; (c) add image view and two labels to your cell prototype, hooking up the @IBOutlets to your CustomCell subclass; and (d) add whatever constraints necessary to define the placement/size of the image view and two labels.

    You can use autolayout constraints to define dimensions of the image view

  3. Your cellForRowAtIndexPath, can then do something like:

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("CustomCell", forIndexPath: indexPath) as! CustomCell
    
        let record = dataSource[indexPath.row]
        cell.customTitleLabel.text = record.Title
        cell.customSubtitleLabel.text = record.SubTitle
        if let urlString = record.Icon {
            cell.customImageView.af_setImageWithURL(NSURL(string: urlString)!)
        }
    
        return cell
    }
    

With that, you enjoy not only basic asynchronous image updating, but also image caching, prioritization of visible images because we're reusing dequeued cell, it's more efficient, etc. And by using a cell prototype with constraints and your custom table view cell subclass, everything is laid out correctly, saving you from manually adjusting the frame in code.

The process is largely the same regardless of which of these UIImageView extensions you use, but the goal is to get you out of the weeds of writing the extension yourself.

于 2015-11-03T17:37:11.530 回答
3

天哪,layoutSubviews 不建议直接使用
,解决问题的正确方法是调用:
[self setNeedsLayout];
[self layoutIfNeeded];
这里,两种方法必须一起调用。
试试这个,祝你好运。

于 2015-11-13T06:24:41.700 回答
2

通过继承 UITableViewCell 创建自己的单元格。您正在使用的样式 .Subtitle 没有图像视图,即使该属性可用。只有样式 UITableViewCellStyleDefault 有图像视图。

于 2015-11-03T16:30:00.870 回答
2

首选 SDWebImages 库这里是链接

它将异步下载图像并缓存图像,也很容易集成到项目中

于 2016-10-07T17:06:13.557 回答