问题
Resources:
I've read multiple answers from Using Auto Layout in UITableView for dynamic cell layouts & variable row heights
And followed their suggestions but it's not working.
Setup to reproduce:
If you copy/paste the MyTableViewCell
and ViewController
snippets: then you can reproduce the issue.
I have subclassed MyTableViewCell and added my own label.
import UIKit
class MyTableViewCell: UITableViewCell {
lazy var customLabel : UILabel = {
let lbl = UILabel()
lbl.translatesAutoresizingMaskIntoConstraints = false
lbl.numberOfLines = 0
return lbl
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupLayout()
}
private func setupLayout(){
contentView.addSubview(customLabel)
let top = customLabel.topAnchor.constraint(equalTo: contentView.topAnchor)
let bottom = customLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
let leadingFromImage = customLabel.leadingAnchor.constraint(equalTo: imageView!.trailingAnchor, constant: 5)
let trailing = customLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
NSLayoutConstraint.activate([top, bottom, leadingFromImage, trailing])
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
}
The following ViewController
contains my tableview:
import UIKit
class ViewController: UIViewController {
var datasource = ["It would have been a great day had Manchester United Lost its \n game. Anyhow I hope tomorrow Arsenal will win the game"]
lazy var tableView : UITableView = {
let table = UITableView()
table.delegate = self
table.dataSource = self
table.translatesAutoresizingMaskIntoConstraints = false
table.estimatedRowHeight = 100
table.rowHeight = UITableViewAutomaticDimension
return table
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.pinToAllEdges(of: view)
tableView.register(MyTableViewCell.self, forCellReuseIdentifier: "id")
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return datasource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "id", for: indexPath) as! MyTableViewCell
cell.customLabel.text = datasource[indexPath.row]
logInfo(of: cell)
cell.accessoryType = .detailDisclosureButton
cell.imageView?.image = UIImage(named: "honey")
cell.layoutSubviews()
cell.customLabel.preferredMaxLayoutWidth = tableView.bounds.width
logInfo(of: cell)
print("---------")
return cell
}
private func logInfo(of cell: MyTableViewCell){
print("boundsWidth: \(cell.contentView.bounds.width) | maxLayoutWidth: \(cell.contentView.bounds.width - 44 - 15 - 5) | systemLayoutSizeFitting : \(cell.customLabel.systemLayoutSizeFitting(UILayoutFittingCompressedSize))")
}
}
extension UIView{
func pinToAllEdges(of view: UIView){
let leading = leadingAnchor.constraint(equalTo: view.leadingAnchor)
let top = topAnchor.constraint(equalTo: view.topAnchor)
let trailing = trailingAnchor.constraint(equalTo: view.trailingAnchor)
let bottom = bottomAnchor.constraint(equalTo: view.bottomAnchor)
NSLayoutConstraint.activate([leading, top, trailing, bottom])
}
}
Link for honey image I used. I've set it's size to 44 * 44
Main issue
My major problem is inside cellForRowAtIndex
:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "id", for: indexPath) as! MyTableViewCell
cell.customLabel.text = datasource[indexPath.row]
logInfo(of: cell)
cell.accessoryType = .detailDisclosureButton
cell.imageView?.image = UIImage(named: "honey")
cell.layoutSubviews()
cell.customLabel.preferredMaxLayoutWidth = cell.contentView.bounds.width
logInfo(of: cell)
print("---------")
return cell
}
Questions:
For whatever reason the value assigned to:
cell.customLabel.preferredMaxLayoutWidth
doesn't seem to be right.
Q1: Why is that?
Q2: I'm logging the contentView's bound before and after I call cell.layoutSubviews
and it switches from 320
to 260
but then eventually in the viewDebugger it shows up as 308
!!!
Why is the contenView's bounds changing again?!
I've removed some other screenshots from the question. They were mostly clutter but maybe worth looking. You can take a look at the revision history.
回答1:
I believe the issue is related to using the default cell's imageView
.
The image view itself doesn't exist until its .image
property is set, so on your cell init you're constraining the custom label to an image view that is 0,0,0,0
Then, in cellForRowAt
, you set the .image
property, and it appears that action also sets the contentView height. I can't find any docs on it, and digging through in debug I can't find any conflicting constraints, so I'm not entirely sure why that's happening.
Two options:
1 - Instead of creating and adding a custom label, set the .numberOfLines
on the default .textLabel
to 0
. That should be enough.
2 - If you need a customized label, also add a custom image view.
Option 2 is here:
class MyTableViewCell: UITableViewCell {
lazy var customLabel : UILabel = {
let lbl = UILabel()
lbl.translatesAutoresizingMaskIntoConstraints = false
lbl.numberOfLines = 0
lbl.setContentCompressionResistancePriority(.required, for: .vertical)
return lbl
}()
lazy var customImageView: UIImageView = {
let v = UIImageView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupLayout()
}
private func setupLayout(){
contentView.addSubview(customLabel)
contentView.addSubview(customImageView)
// constrain leading of imageView to be 15-pts from the leading of the contentView
let imgViewLeading = customImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15)
// constrain width of imageView to 42-pts
let imgViewWidth = customImageView.widthAnchor.constraint(equalToConstant: 42)
// constrain height of imageView to be equal to width of imageView
let imgViewHeight = customImageView.heightAnchor.constraint(equalTo: customImageView.widthAnchor, multiplier: 1.0)
// center imageView vertically
let imgViewCenterY = customImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0.0)
// top and bottom constraints for the imageView also need to be set,
// otherwise the image will exceed the height of the cell when there
// is not enough text to wrap and expand the height of the label
// constrain top of imageView to be *at least* 4-pts from the top of the cell
let imgViewTop = customImageView.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: 4)
// constrain bottom of imageView to be *at least* 4-pts from the bottom of the cell
let imgViewBottom = customImageView.topAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -4)
// constrain top of the label to be *at least* 4-pts from the top of the cell
let top = customLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4)
// if you want the text in the label vertically centered in the cell
// constrain bottom of the label to be *exactly* 4-pts from the bottom of the cell
let bottom = customLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4)
// if you want the text in the label top-aligned in the cell
// constrain bottom of the label to be *at least* 4-pts from the bottom of the cell
// let bottom = customLabel.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -4)
// constrain leading of the label to be 5-pts from the trailing of the image
let leadingFromImage = customLabel.leadingAnchor.constraint(equalTo: customImageView.trailingAnchor, constant: 5)
// constrain the trailing of the label to the trailing of the contentView
let trailing = customLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
NSLayoutConstraint.activate([
top, bottom, leadingFromImage, trailing,
imgViewLeading, imgViewCenterY, imgViewWidth, imgViewHeight,
imgViewTop, imgViewBottom
])
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
}
class HoneyViewController: UIViewController {
var datasource = [
"It would have been a great day had Manchester United Lost its game. Anyhow I hope tomorrow Arsenal will win the game",
"One line.",
"Two\nLines.",
]
lazy var tableView : UITableView = {
let table = UITableView()
table.delegate = self
table.dataSource = self
table.translatesAutoresizingMaskIntoConstraints = false
table.estimatedRowHeight = 100
table.rowHeight = UITableViewAutomaticDimension
return table
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.pinToAllEdges(of: view)
tableView.register(MyTableViewCell.self, forCellReuseIdentifier: "id")
}
}
extension HoneyViewController: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return datasource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "id", for: indexPath) as! MyTableViewCell
cell.customLabel.text = datasource[indexPath.row]
logInfo(of: cell)
cell.accessoryType = .detailDisclosureButton
cell.customImageView.image = UIImage(named: "Honey")
logInfo(of: cell)
print("---------")
return cell
}
private func logInfo(of cell: MyTableViewCell){
print("boundsWidth: \(cell.contentView.bounds.width) | maxLayoutWidth: \(cell.contentView.bounds.width - 44 - 15 - 5) | systemLayoutSizeFitting : \(cell.customLabel.systemLayoutSizeFitting(UILayoutFittingCompressedSize))")
}
}
extension UIView{
func pinToAllEdges(of view: UIView){
let leading = leadingAnchor.constraint(equalTo: view.leadingAnchor)
let top = topAnchor.constraint(equalTo: view.topAnchor)
let trailing = trailingAnchor.constraint(equalTo: view.trailingAnchor)
let bottom = bottomAnchor.constraint(equalTo: view.bottomAnchor)
NSLayoutConstraint.activate([leading, top, trailing, bottom])
}
}
Edit:
A couple more constraints are needed. If the cell has only enough text for one line (no wrapping), the imageView height will exceed the height of the cell:
So, we add top and bottom constraints to the imageView to fit at least the top and bottom of the cell:
and, it will probably look a little better with some padding, so we constrain the top and bottom of the imageView to be at least 4-pts from the top and bottom of the cell:
If desired, we can also "top-align" the text in the label by constraining its bottom to be at least 4-pts from the bottom, instead of exactly 4-pts from the bottom:
The comments in my edited code should explain each of those differences.
来源:https://stackoverflow.com/questions/52707917/how-to-correctly-get-tableviewcells-contentview-bound-size