Swift - TableViewCell 中不同大小的图像的困难

Posted

技术标签:

【中文标题】Swift - TableViewCell 中不同大小的图像的困难【英文标题】:Swift - Difficulty with different sized images in TableViewCell 【发布时间】:2019-03-12 18:32:24 【问题描述】:

我正在使用 Kingfisher 加载许多远程图像,并且很难将它们正确加载到具有动态高度单元格的 Tableview 中。 我的目标是让图像始终是屏幕的全宽和动态高度,如何实现?

我之前问了一个相关问题,这导致使用堆栈视图了解基本布局:SnapKit: How to set layout constraints for items in a TableViewCell programatically

所以我构建了如下内容:

使用以下代码(为简洁起见,删除了一些部分):

// CREATE VIEWS
let containerStack = UIStackView()
let header = UIView()
let headerStack = UIStackView()
let title = UILabel()
let author = UILabel()
var previewImage = UIImageView()

...

// KINGFISHER
let url = URL(string: article.imageUrl)
previewImage.kf.indicatorType = .activity
previewImage.kf.setImage(
  with: url,
  options: [
    .transition(.fade(0.2)),
    .scaleFactor(UIScreen.main.scale),
    .cacheOriginalImage
])  result in
  switch result 
  case .success(_):
    self.setNeedsLayout()
    UIView.performWithoutAnimation 
      self.tableView()?.beginUpdates()
      self.tableView()?.endUpdates()
    
  case .failure(let error):
    print(error)
  


...

// LAYOUT
containerStack.axis = .vertical

headerStack.axis = .vertical
headerStack.spacing = 6
headerStack.addArrangedSubview(title)
headerStack.addArrangedSubview(author)
header.addSubview(headerStack)

containerStack.addArrangedSubview(header)
containerStack.addSubview(previewImage)

addSubview(containerStack)

headerStack.snp.makeConstraints  make in
  make.edges.equalToSuperview().inset(20)


containerStack.snp.makeConstraints  make in
  make.edges.equalToSuperview()

没有imageView 的约束,图像不会出现。

使用以下约束,图像也不会出现:

previewImage.snp.makeConstraints  make in
  make.leading.trailing.bottom.equalToSuperview()
  make.top.equalTo(headerView.snp.bottom).offset(20)

通过其他尝试,图像完全倾斜或与标签/其他单元格和图像重叠。

最后,遵循此评论:With Auto Layout, how do I make a UIImageView's size dynamic depending on the image? 和此要点:https://gist.github.com/marcc-orange/e309d86275e301466d1eecc8e400ad00 以及这些约束 make.edges.equalToSuperview() 我能够让图像以正确的比例显示,但它们完全覆盖了标签。

理想情况下应该是这样的:

【问题讨论】:

你没有做正确的约束。 @ElTomato 是的,我认为这是约束的问题,再加上远程图像的一些复杂性等,您对如何最好地进行有什么建议吗?我还认为 StackViews 应该减少对大多数约束的需求。 用你的约束代码替换你显示的代码。翠鸟与你的问题没有任何关系。一般来说,您可以摆脱外部堆栈视图和标题视图。您可能还想阅读有关内容拥抱和抗压缩优先级的文档。 @MartinM 对不起,我不完全确定您所说的“用约束代码替换您显示的代码”是什么意思。我觉得我尝试的所有限制都是有问题的。 (或者你的意思是隐藏翠鸟代码,因为它不相关)。在链接的线程中,提到使用StackView的人,否则,它怎么知道在单元格内排列项目? 在您的服务中添加 aspect ratiowidth 字段 【参考方案1】:

带有示例代码的 100 % 工作解决方案

我只是设法通过动态标签内容和动态图像尺寸实现了相同的布局。我通过约束和自动布局做到了。看看这个GitHub Repository的演示项目@


正如马特所指出的,我们必须在图像下载后计算每个单元格的高度(当我们知道它的宽度和高度时)。注意每个单元格的高度是通过tableView的委托方法heightForRowAt IndexPath计算出来的

因此,在下载每个图像后,将图像保存在 indexPath 的数组中并重新加载该 indexPath,以便根据图像尺寸再次计算高度。

需要注意的一些关键点如下

使用 3 种类型的单元格。一个用于标签,一个用于字幕,一个用于图像。在cellForRowAt 内部初始化并返回相应的 细胞。每个单元格都有一个唯一的cellIdentifier,但类是相同的 tableView 中的节数 == 数据源计数 节中的行数 == 3 第一行对应标题,第二行对应副标题,第三行对应图片。 标签的行数应为 0,以便根据内容计算高度 在cellForRowAt 内部异步下载图像,将其存储在数组中并重新加载该行。 通过重新加载行,调用heightForRowAt,根据图像尺寸计算所需的单元格高度并返回高度。 因此每个单元格的高度都是根据图像尺寸动态计算的

看看一些代码

override func numberOfSections(in tableView: UITableView) -> Int 
  return arrayListItems.count


override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int 
  //Title, SubTitle, and Image
  return 3


override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 

switch indexPath.row 
case 0:
  //configure and return Title Cell. See code in Github Repo 

case 1:

  //configure and return SubTitle Cell. See code in Github Repo

case 2:

  let cellImage = tableView.dequeueReusableCell(withIdentifier: cellIdentifierImage) as! TableViewCell
  let item = arrayListItems[indexPath.section]
  //if we already have the image, just show
  if let image = arrayListItems[indexPath.section].image 
    cellImage.imageViewPicture.image = image
  else 

    if let url = URL.init(string: item.imageUrlStr) 

      cellImage.imageViewPicture.kf.setImage(with: url)  [weak self] result in
        guard let strongSelf = self else  return  //arc
        switch result 
        case .success(let value):

          print("=====Image Size \(value.image.size)"  )
          //store image in array so that `heightForRowAt` can use image width and height to calculate cell height
          strongSelf.arrayListItems[indexPath.section].image = value.image
          DispatchQueue.main.async 
          //reload this row so that `heightForRowAt` runs again and calculates height of cell based on image height
            self?.tableView.reloadRows(at: [indexPath], with: .automatic)
          

        case .failure(let error):
          print(error) // The error happens
        
      

    

  


  return cellImage

default:
  print("this should not be called")


//this should not be executed
return .init()



override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat 
//calculate the height of label cells automatically in each section
if indexPath.row == 0 || indexPath.row == 1  return UITableView.automaticDimension 

// calculating the height of image for indexPath
else if indexPath.row == 2, let image = arrayListItems[indexPath.section].image 

  print("heightForRowAt indexPath : \(indexPath)")
  //image

  let imageWidth = image.size.width
  let imageHeight = image.size.height

  guard imageWidth > 0 && imageHeight > 0 else  return UITableView.automaticDimension 

  //images always be the full width of the screen
  let requiredWidth = tableView.frame.width

  let widthRatio = requiredWidth / imageWidth

  let requiredHeight = imageHeight * widthRatio

  print("returned height \(requiredHeight) at indexPath: \(indexPath)")
  return requiredHeight



else  return UITableView.automaticDimension 


相关。

我们可以遵循的另一种方法是从 API 请求中返回图像尺寸。如果可以做到这一点,它将简化很多事情。看看这个类似的问题(针对collectionView)。

Self sizing Collection view cells with async image downloading.

Placholder.com用于异步获取图片

Self Sizing Cells: (A Good read)

样本

【讨论】:

【参考方案2】:

按照您的描述进行操作相对容易:您的图像视图需要一个等于“屏幕”宽度(如您所说)的宽度约束和一个与宽度约束成比例的高度约束( multiplier) 基于下载图像的比例(又名“纵横比”)。该值不能提前设置;下载图像后,您需要对其进行配置,因为直到那时您才知道其比例。因此,您需要一个高度约束的出口,以便您可以将其移除,并在您知道它时将其替换为具有正确multiplier 的出口。如果您的其他约束相对于图像视图的顶部和底部是正确的,则其他所有内容都将按照需要进行。

这些屏幕截图表明这种方法有效:

(进一步向下滚动表格视图:)

它与您想要的界面并非 100% 相同,但想法是相同的。在每个单元格中,我们有两个标签和一个图像,图像可以有不同的纵横比,但这些纵横比可以正确显示 - 并且单元格本身具有不同的高度。

这是我使用的关键代码:

    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! Cell
    // in real life you’d set the labels here too
    // in real life you’d be fetching the image from the network...
    // ...and probably supplying it asynchronously later
    let im = UIImage(named:self.pix[indexPath.row])!
    cell.iv.image = im
    let con = cell.heightConstraint!
    con.isActive = false
    let ratio = im.size.width/im.size.height
    let newcon = NSLayoutConstraint(item: con.firstItem, attribute: con.firstAttribute, relatedBy: con.relation, toItem: con.secondItem, attribute: con.secondAttribute, multiplier: ratio, constant: 0)
    newcon.isActive = true
    cell.heightConstraint = newcon
    return cell

【讨论】:

正如其他人所说,这是对堆栈视图的一种愚蠢使用,因此我建议您摆脱它。 谢谢回复,所以外层栈视图不应该用吗?那么 ImageView 是否还需要将约束放置在标签底部下方? (提醒一下,这些都在 TableViewCells 中,以防万一) 添加了屏幕截图和一些代码。在现实生活中,事情对您来说会有点复杂,因为您将异步获取图像,但正如其他人指出的那样,这与您所询问的问题无关。 添加和删除约束是繁重的操作,最好用手计算期望的高度(因为你已经有了比例和宽度)并且只更新高度约束的常量 @ManWithBear 那是错误的。有一个关于约束性能的精彩 WWDC 2028 视频。观看。【参考方案3】:

如果您不想更改布局,有一个直接的解决方案可以解决您的问题。

1- 定义你的细胞

2- 将 UIImageView 和您喜欢的其他 UI 元素放入单元格中,并为图像视图添加以下约束: - 顶部、前导、尾随、底部到超级视图 -height 约束并在代码中添加出口(例如:heightConstraint)

3-将内容填充更改为:aspect fit

4- 通过 kingfisher 或您喜欢的任何其他方式加载您的图像,一旦您传递您的图像,检查大小并计算比率:imageAspectRatio = height/width

5-设置heightConstraint.constant = screenWidth * imageAspectRatio

6-call layoutIfNeeded() 为单元格,你应该没问题!

*此解决方案适用于任何 UI 布局组合,包括堆栈视图,重点是对图像有约束,并让 tableview 弄清楚如何计算和绘制约束。

class CustomTableViewCell: UITableViewCell 

    @IBOutlet weak var heightConstraint: NSLayoutConstraint!
    @IBOutlet weak var sampleImageView: UIImageView!

    override func awakeFromNib() 
        super.awakeFromNib()
        // Initialization code
    

    func configure(image:UIImage) 
        let hRatio = image.size.height / image.size.width
        let newImageHeight = hRatio * UIScreen.main.bounds.width
        heightConstraint.constant = newImageHeight
        sampleImageView.image = image
        sampleImageView.layoutIfNeeded()
    

结果:

【讨论】:

这是建议的最佳方法。谢谢!

以上是关于Swift - TableViewCell 中不同大小的图像的困难的主要内容,如果未能解决你的问题,请参考以下文章

如何仅在 swift 中重新加载 tableViewCell?

为啥我在 swift 中调用 getIndex(cell: TableViewCell1) 函数时出错?

在 Swift 中编辑 TableViewCell 的内容列表

滚动时 TableViewCell 中的 Swift 3 UISwitch 丢失状态

如何在 Swift 3.0 中选择 JSON 输出到 TableViewCell 中?

在 TableViewCell 中保持 UISwitch 的状态:Swift