在iOS 8中,蘋果引入了UITableView的一項新功能--Self Sizing Cells,對於不少開發者來說這是新SDK中一項非常有用的新功能。在iOS 8之前,如果想在表視圖中展示可變高度的動態內容時,你需要手動計算行高,而Self Sizing Cells為展示動態內容提供了一個解決方... ...
UITableView
我們都知道UITableView從iOS 8開始實現行高的自適應相對比較簡單,首先必須設置estimatedRowHeight給出預估高度,設置rowHeight為UITableViewAutomaticDimension(註意:如果不修改rowHeight預設就是UITableViewAutomaticDimension),對於這兩個參數除了直接修改tableview對應的屬性之外仍然支持使用對應的代理方法設置。最後只要在UITableViewCell中設置contentView的約束即可。由於UITableViewCell的寬度等同於UITableView因此約束的設置事實上只是為了自動計算高度。通常的做法就是設置contentView的top和bottom約束,而後其內部子視圖可以提供intrinsicContentSize(例如UIButtonUILabel預設就已經提供)或者已經有明確的height約束。這樣一來就可以做到子控制項確定了自身高度,而contentView子控制項又設置了和contentView相關的bottom約束來反向計算出UITableViewCell的實際高度。
下麵仍然以前面UITableView文章的自定義Cell舉例,相比之前大量的運算而言Self-Sizing Cells可以說簡化了很多。除了設置estimatedRowHeight外最重要的就是添加相關Autolayout約束。由於頭像高度已經固定,內容高度可以通過固有高度自動計算,而二者的間隔和top、bottom約束已經固定,從而Self-Sizing Cells可以自動計算出Cell的高度。
高度計算約束關係:
Cell佈局代碼:
import UIKit
import SnapKit
class StatusTableViewCell: UITableViewCell {
// MARK: - 公共屬性
var status:Status! {
didSet {
self.avatarImageView.image = UIImage(named: status.profileImageUrl)
self.userNameLabel.text = status.userName
self.mtypeImageView.image = UIImage(named: status.mbtype)
self.createdAtLabel.text = status.createdAt
self.sourceLabel.text = status.source
self.contentLabel.text = status.text
}
}
// MARK: - 生命周期及方法覆蓋
override func awakeFromNib() {
super.awakeFromNib()
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func setSelected(_ selected: Bool, animated: Bool) {
}
// MARK: - 私有方法
private func setup() {
self.contentView.addSubview(self.avatarImageView)
self.contentView.addSubview(self.userNameLabel)
self.contentView.addSubview(self.mtypeImageView)
self.contentView.addSubview(self.createdAtLabel)
self.contentView.addSubview(self.sourceLabel)
self.contentView.addSubview(self.contentLabel)
self.avatarImageView.snp.makeConstraints { (make) in
make.top.left.equalTo(10.0)
make.size.equalTo(CGSize(width: 40.0, height: 40.0))
}
self.userNameLabel.snp.makeConstraints { (make) in
make.top.equalTo(self.avatarImageView.snp.top)
make.left.equalTo(self.avatarImageView.snp.right).offset(8.0)
}
self.mtypeImageView.snp.makeConstraints { (make) in
make.top.equalTo(self.userNameLabel.snp.top)
make.left.equalTo(self.userNameLabel.snp.right).offset(8.0)
make.size.equalTo(CGSize(width: 14.0, height: 14.0))
}
self.createdAtLabel.snp.makeConstraints { (make) in
make.left.equalTo(self.userNameLabel.snp.left)
make.bottom.equalTo(self.avatarImageView.snp.bottom)
}
self.sourceLabel.snp.makeConstraints { (make) in
make.left.equalTo(self.createdAtLabel.snp.right).offset(10.0)
make.bottom.equalTo(self.createdAtLabel.snp.bottom)
make.right.lessThanOrEqualTo(-8.0)
}
self.contentLabel.snp.makeConstraints { (make) in
make.top.equalTo(self.avatarImageView.snp.bottom).offset(8.0)
make.left.equalTo(self.avatarImageView.snp.left)
make.right.equalTo(-8.0)
make.bottom.equalTo(-10.0) // 註意此處必須設置,否則contentView無法自適應高度
}
}
// MARK: - 私有屬性
private lazy var avatarImageView:UIImageView = {
let temp = UIImageView()
return temp
}()
private lazy var mtypeImageView:UIImageView = {
let temp = UIImageView()
return temp
}()
private lazy var userNameLabel:UILabel = {
let temp = UILabel()
temp.textColor = UIColor(red: 50.0/255.0, green: 50.0/255.0, blue: 50.0/255.0, alpha: 1.0)
temp.font = UIFont.systemFont(ofSize: 14.0)
return temp
}()
private lazy var createdAtLabel:UILabel = {
let temp = UILabel()
temp.textColor = UIColor(red: 120.0/255.0, green: 120.0/255.0, blue: 120.0/255.0, alpha: 1.0)
temp.font = UIFont.systemFont(ofSize: 12.0)
return temp
}()
private lazy var sourceLabel:UILabel = {
let temp = UILabel()
temp.textColor = UIColor(red: 120.0/255.0, green: 120.0/255.0, blue: 120.0/255.0, alpha: 1.0)
temp.font = UIFont.systemFont(ofSize: 12.0)
return temp
}()
private lazy var contentLabel:UILabel = {
let temp = UILabel()
temp.textColor = UIColor(red: 50.0/255.0, green: 50.0/255.0, blue: 50.0/255.0, alpha: 1.0)
temp.font = UIFont.systemFont(ofSize: 14.0)
temp.numberOfLines = 0
return temp
}()
}
最終效果:
無論是UITableView還是後面的UICollectionview,Self-Sizing Cells的概念均是從iOS 8開始提出的。如果是iOS 8之前的版本則需要通過systemLayoutSizeFitting()進行計算或者通過frame直接設置。
UICollectionView
瞭解了UITableView的Cell行高自適應之後,要理解UICollectionviewCell的Size自適應並不難,因為UICollectionViewCell相比較於UITableViewCell除了要通過AutoLayout確定contentView的高度之外還要確定其寬度,其寬度確定原則和UITableViewCell的高度確定是類似的,只是要通過UICollectionviewCell的contentView子控制項自身確定其寬度,然後設置子控制項和contentView相關的right約束即可。當然對於UICollectionViewCell自適應尺寸同樣必須設置UICollectionViewFlowLayout的estimatedItemSize屬性(如果使用UICollectionViewFlowLayout)。
下麵的的demo演示了類淘寶商品展示的UICollectionview佈局,除了通過AutoLayout確定高度外,通過商品圖片的left、right約束和width的設置可以反向推斷出contentView的寬度,通過Self-Sizing Cells就可以最終確定UICollectionviewCell的size。
Cell佈局代碼:
import UIKit
class ProductCollectionViewCell: UICollectionViewCell {
// MARK: - 公共屬性
var product:Product! {
didSet {
self.productImageView.image = UIImage(named: product.image)
self.contentLabel.text = product.text
self.priceLabel.text = "¥\(product.price)"
self.salesLabel.text = "\(product.sale)人購買"
}
}
// MARK: - 生命周期及方法覆蓋
override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
return super.preferredLayoutAttributesFitting(layoutAttributes)
}
// MARK: - 私有方法
private func setup() {
self.backgroundColor = UIColor.white
self.contentView.addSubview(self.productImageView)
self.contentView.addSubview(self.contentLabel)
self.contentView.addSubview(self.priceLabel)
self.contentView.addSubview(self.salesLabel)
let screenWidth = UIScreen.main.bounds.width
self.productImageView.snp.makeConstraints { (make) in
make.top.left.right.equalTo(0.0)
make.height.equalTo(screenWidth*0.5).priority(999)
make.width.equalTo((screenWidth-18)*0.5).priority(999) // 此設置可以確定cell寬度,註意儘管降低了預設的優先順序僅僅是為了計算中間步驟不至於約束衝突,最終顯示時此約束仍然會生效
}
self.contentLabel.snp.makeConstraints { (make) in
make.top.equalTo(self.productImageView.snp.bottom).offset(4.0)
make.left.equalTo(8.0)
make.right.equalTo(-8.0)
make.height.equalTo(28.0)
}
self.priceLabel.snp.makeConstraints { (make) in
make.top.equalTo(self.contentLabel.snp.bottom).offset(8.0)
make.left.equalTo(self.contentLabel.snp.left)
make.bottom.equalTo(-8.0) //此設置可以確定cell高度
}
self.salesLabel.snp.makeConstraints { (make) in
make.centerY.equalTo(self.priceLabel.snp.centerY)
make.right.equalTo(-8.0)
}
}
// MARK: - 私有屬性
private lazy var productImageView:UIImageView = {
let temp = UIImageView()
temp.contentMode = .scaleAspectFit
temp.clipsToBounds = true
return temp
}()
private lazy var contentLabel:UILabel = {
let temp = UILabel()
temp.textColor = UIColor(red: 50.0/255.0, green: 50.0/255.0, blue: 50.0/255.0, alpha: 1.0)
temp.font = UIFont.systemFont(ofSize: 12.0)
temp.numberOfLines = 2
return temp
}()
private lazy var priceLabel:UILabel = {
let temp = UILabel()
temp.textColor = UIColor.orange
temp.font = UIFont.systemFont(ofSize: 14.0)
return temp
}()
private lazy var salesLabel:UILabel = {
let temp = UILabel()
temp.textColor = UIColor(red: 150.0/255.0, green: 150.0/255.0, blue: 150.0/255.0, alpha: 1.0)
temp.font = UIFont.systemFont(ofSize: 12.0)
return temp
}()
}
最終效果:
除此之外從iOS 8開始,UICollectionViewCell提供了preferredLayoutAttributesFitting()方法用於提供一些cell屬性修改,當然通過此方法可以重新修改cell的size(包括Self-Sizing Cells自動計算後的size),對於手動計算高度的情況這也方法也提供了一種不用外部設置cell size的而能提供讓UICollectionView確定cell尺寸的方式,但是這並不在Self-Sizing Cells討論之列,因此本文不再深入討論。