Swift - 在 TableViewCell 中使用不同大小的图像有困难
Swift - Difficulty with different sized images in TableViewCell
我正在使用 Kingfisher 加载大量远程图像,但很难将它们正确加载到具有动态高度单元格的 Tableview 中。 我的目标是让图像始终是屏幕的全宽和动态高度,如何实现?
我之前问过一个相关问题,它让我理解了使用堆栈视图的基本布局:
所以我构建了如下内容:
使用以下代码(为简洁起见删除了一些部分):
// 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)
}
在其他尝试中,图像完全倾斜或与 labels/other 单元格和图像重叠。
最后,按照以下评论:With Auto Layout, how do I make a UIImageView's size dynamic depending on the image? and this gist: https://gist.github.com/marcc-orange/e309d86275e301466d1eecc8e400ad00 并且在这些限制下 make.edges.equalToSuperview()
我能够让图像以正确的比例显示,但它们完全覆盖了标签。
理想情况下它看起来像这样:
做你所描述的事情相对容易:你的图像视图需要一个等于“屏幕”宽度的宽度约束(如你所说)和一个与宽度成比例的高度约束基于下载图像比例(又名“纵横比”)的约束(multiplier
)。这个值不能预先设置;一旦下载了图像,您就需要对其进行配置,因为直到那时您才知道它的比例。因此,您需要一个高度限制的出口,以便您可以将其移除并在知道时用具有正确 multiplier
的出口替换它。如果您的其他约束相对于图像视图的顶部和底部是正确的,则其他所有内容都将按照需要进行。
这些屏幕截图表明此方法有效:
(进一步向下滚动 table 视图:)
它不是 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
如果您不想更改布局,有一个直接的解决方案可以解决您的问题。
1- 定义你的单元格
2- 将您喜欢的 UIImageView
和其他 UI 元素放入您的单元格中,并为图像视图添加这些约束:
- 顶部,前导,尾随,底部到超级视图
-高度限制并在您的代码中添加出口(例如:heightConstraint)
3-将内容填充更改为:纵横比
4- 通过 kingfisher 或您喜欢的任何其他方式加载您的图像,一旦您通过图像,检查尺寸并计算比率:imageAspectRatio = height/width
5-设置heightConstraint.constant = screenWidth * imageAspectRatio
为单元格调用 layoutIfNeeded() 6 次,你应该没问题!
*此解决方案适用于任何 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()
}
}
结果:
100% 工作解决方案,带有示例代码
我刚刚设法实现了具有动态标签内容和动态图像尺寸的相同布局。我是通过约束和 Autolayout 做到的。看看这个 GitHub Repository
的演示项目
正如 matt 指出的那样,我们必须在下载图像后计算每个单元格的高度(当我们知道它的宽度和高度时)。注意每个cell的高度是通过tableView的delegate方法计算出来的heightForRowAt IndexPath
因此,在下载完每张图片后,将图片保存在该索引路径的数组中,然后重新加载该索引路径,以便根据图片尺寸再次计算高度。
需要注意的几个要点如下
- Use 3 types of cells. One for label, one for subtitle and one for Image. Inside
cellForRowAt
initialize and return the appropriate
cell. Each cell has a unique cellIdentifier
but class is same
- number of sections in tableView == count of data source
- number of rows in section == 3
- First row corresponds to title, second row corresponds to subtitle and the 3rd corresponds to the image.
- number of lines for labels should be 0 so that height should be calculated based on content
- Inside
cellForRowAt
download the image asynchrounously, store it in array and reload that row.
- By reloading the row,
heightForRowAt
gets called, calculates the required cell height based on image dimensions and returns the height.
- So each cell's height is calculated dynamically based on image dimensions
看看一些代码
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 }
}
相关。
我们可以遵循的另一种方法是 return 来自 API 请求的图像尺寸。如果能做到这一点,事情就会简单很多。看看这个类似的问题(对于 collectionView)。
Placholder.com 用于异步获取图片
Self Sizing Cells: (A Good read)
样本
我正在使用 Kingfisher 加载大量远程图像,但很难将它们正确加载到具有动态高度单元格的 Tableview 中。 我的目标是让图像始终是屏幕的全宽和动态高度,如何实现?
我之前问过一个相关问题,它让我理解了使用堆栈视图的基本布局:
所以我构建了如下内容:
使用以下代码(为简洁起见删除了一些部分):
// 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)
}
在其他尝试中,图像完全倾斜或与 labels/other 单元格和图像重叠。
最后,按照以下评论:With Auto Layout, how do I make a UIImageView's size dynamic depending on the image? and this gist: https://gist.github.com/marcc-orange/e309d86275e301466d1eecc8e400ad00 并且在这些限制下 make.edges.equalToSuperview()
我能够让图像以正确的比例显示,但它们完全覆盖了标签。
理想情况下它看起来像这样:
做你所描述的事情相对容易:你的图像视图需要一个等于“屏幕”宽度的宽度约束(如你所说)和一个与宽度成比例的高度约束基于下载图像比例(又名“纵横比”)的约束(multiplier
)。这个值不能预先设置;一旦下载了图像,您就需要对其进行配置,因为直到那时您才知道它的比例。因此,您需要一个高度限制的出口,以便您可以将其移除并在知道时用具有正确 multiplier
的出口替换它。如果您的其他约束相对于图像视图的顶部和底部是正确的,则其他所有内容都将按照需要进行。
这些屏幕截图表明此方法有效:
(进一步向下滚动 table 视图:)
它不是 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
如果您不想更改布局,有一个直接的解决方案可以解决您的问题。
1- 定义你的单元格
2- 将您喜欢的 UIImageView
和其他 UI 元素放入您的单元格中,并为图像视图添加这些约束:
- 顶部,前导,尾随,底部到超级视图
-高度限制并在您的代码中添加出口(例如:heightConstraint)
3-将内容填充更改为:纵横比
4- 通过 kingfisher 或您喜欢的任何其他方式加载您的图像,一旦您通过图像,检查尺寸并计算比率:imageAspectRatio = height/width
5-设置heightConstraint.constant = screenWidth * imageAspectRatio
为单元格调用 layoutIfNeeded() 6 次,你应该没问题!
*此解决方案适用于任何 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()
}
}
结果:
100% 工作解决方案,带有示例代码
我刚刚设法实现了具有动态标签内容和动态图像尺寸的相同布局。我是通过约束和 Autolayout 做到的。看看这个 GitHub Repository
的演示项目正如 matt 指出的那样,我们必须在下载图像后计算每个单元格的高度(当我们知道它的宽度和高度时)。注意每个cell的高度是通过tableView的delegate方法计算出来的heightForRowAt IndexPath
因此,在下载完每张图片后,将图片保存在该索引路径的数组中,然后重新加载该索引路径,以便根据图片尺寸再次计算高度。
需要注意的几个要点如下
- Use 3 types of cells. One for label, one for subtitle and one for Image. Inside
cellForRowAt
initialize and return the appropriate cell. Each cell has a uniquecellIdentifier
but class is same- number of sections in tableView == count of data source
- number of rows in section == 3
- First row corresponds to title, second row corresponds to subtitle and the 3rd corresponds to the image.
- number of lines for labels should be 0 so that height should be calculated based on content
- Inside
cellForRowAt
download the image asynchrounously, store it in array and reload that row.- By reloading the row,
heightForRowAt
gets called, calculates the required cell height based on image dimensions and returns the height.- So each cell's height is calculated dynamically based on image dimensions
看看一些代码
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 }
}
相关。
我们可以遵循的另一种方法是 return 来自 API 请求的图像尺寸。如果能做到这一点,事情就会简单很多。看看这个类似的问题(对于 collectionView)。
Placholder.com 用于异步获取图片
Self Sizing Cells: (A Good read)