「 UITableView 入門 」新手解決列表 Cell 高度自適應(yīng),UITableViewCell 高度自適應(yīng)問(wèn)題

一、前言

  • 我們?cè)趯懥斜淼臅r(shí)候,經(jīng)常出現(xiàn)每一個(gè) Cell 高度不一樣的情況,但是 iOS 這邊是在是太不智能了
  • 比起隔壁 androidRecyclerView ,人家可以自動(dòng)更具每一項(xiàng)高度,來(lái)進(jìn)行伸縮變化,iOS 的列表控件 UITableView 竟然都不能直接自適應(yīng)列表高度

二、效果

  • 其實(shí)具體的實(shí)現(xiàn)并不難,只是沒(méi)學(xué)過(guò)的人肯定搞不出來(lái),開(kāi)始前這里可以先看下效果
  • 大致就是 UITableView 會(huì)自動(dòng)計(jì)算每一個(gè) cell 的高度,伸縮變換后顯示出來(lái),網(wǎng)上有很多類似的帖子,但是大都紙上談兵,沒(méi)圖沒(méi)代碼地講不清楚,而且還都是 n 年前的文章
  • 那么開(kāi)始前。效果圖這里效果圖如下:


    image

三、使用與實(shí)現(xiàn)

  • 就以上圖為例,我?guī)Т蠹疫呏v解邊實(shí)現(xiàn)上圖中的例子,這樣一來(lái)成功運(yùn)行的時(shí)候,大家就也都會(huì)了

3.1 實(shí)現(xiàn)數(shù)據(jù)提供者 - ContentProvider

  • 實(shí)現(xiàn)一個(gè)數(shù)據(jù)提供者 - ContentProvider ,用于模擬從網(wǎng)絡(luò)上拉去數(shù)據(jù)的情況
class ContentProvider {
    
    static let datas = ["對(duì)我個(gè)人而言,美麗的沙灘不僅僅是一個(gè)重大的事件,還可能會(huì)改變我的人生。",
                 "美麗的沙灘因何而發(fā)生? 我認(rèn)為, 那么, 查爾斯·史考伯在不經(jīng)意間這樣說(shuō)過(guò),一個(gè)人幾乎可以在任何他懷有無(wú)限熱忱的事情上成功。",
                 "對(duì)我個(gè)人而言,美麗的沙灘不僅僅是一個(gè)重大的事件,還可能會(huì)改變我的人生。 帶著這些問(wèn)題,我們來(lái)審視一下美麗的沙灘。 美麗的沙灘,發(fā)生了會(huì)如何,不發(fā)生又會(huì)如何。 帶著這些問(wèn)題,我們來(lái)審視一下美麗的沙灘。 既然如何, 我認(rèn)為, 而這些并不是完全重要,更加重要的問(wèn)題是, 這樣看來(lái), 帶著這些問(wèn)題,我們來(lái)審視一下美麗的沙灘。",
                 "我們都知道,只要有意義,那么就必須慎重考慮。",
                 "對(duì)我個(gè)人而言,美麗的沙灘不僅僅是一個(gè)重大的事件,還可能會(huì)改變我的人生。",
                 "美麗的沙灘因何而發(fā)生? 我認(rèn)為, 那么, 查爾斯·史考伯在不經(jīng)意間這樣說(shuō)過(guò),一個(gè)人幾乎可以在任何他懷有無(wú)限熱忱的事情上成功。",
                 "莎士比亞說(shuō)過(guò)一句富有哲理的話,人的一生是短的,但如果卑劣地過(guò)這一生,就太長(zhǎng)了。這似乎解答了我的疑惑。 帶著這些問(wèn)題,我們來(lái)審視一下美麗的沙灘。 ",
                 "一般來(lái)說(shuō), 既然如此, 這樣看來(lái), 我們都知道,只要有意義,那么就必須慎重考慮。 每個(gè)人都不得不面對(duì)這些問(wèn)題。 在面對(duì)這種問(wèn)題時(shí), 了解清楚美麗的沙灘到底是一種怎么樣的存在,是解決一切問(wèn)題的關(guān)鍵。",
                 "我們都知道,只要有意義,那么就必須慎重考慮。"]
    
    static let imgs = ["paperplane.fill","square.and.arrow.down","paperplane.fill","bell","square.and.arrow.down","bell","paperplane.fill","bell","square.and.arrow.down"]
    
}

這里節(jié)約時(shí)間,就不做異步拉取的處理了,后續(xù)文章我會(huì)擠時(shí)間,專門搞一篇 UITableView 異步請(qǐng)求加觀察者模式的文章來(lái)給大家分享

3.2 編寫列表 item - UITableViewCell

  • 要讓 cell 隨自身內(nèi)容大小而變化高度,只需要注意三點(diǎn)即可
  • 首先是,addSubView 必須是添加到 contentView 上,而非簡(jiǎn)單的 self
  • 其次是,內(nèi)部組件必須設(shè)置 translatesAutoresizingMaskIntoConstraints 屬性為 true
  • 最后是,這個(gè) cell 不能通過(guò)簡(jiǎn)單的 frame 設(shè)置大小,而是需要通過(guò) NSLayoutConstraint 來(lái)動(dòng)態(tài)給定
  • 首先這里我先提供下最終實(shí)現(xiàn)的代碼再逐個(gè)給大家分析:
import Foundation
import UIKit

class MemberCell: UITableViewCell {
    
    lazy var contentLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        self.contentView.addSubview(label)
        label.numberOfLines = 0
        return label
    }()
    
    lazy var userImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.translatesAutoresizingMaskIntoConstraints = false
        self.contentView.addSubview(imageView)
        imageView.image = UIImage(systemName: "Camera")
        return imageView
    }()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupConstraint()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setupConstraint() {
        userImageView.accessibilityIdentifier = "userImageView"
        contentLabel.accessibilityIdentifier = "profileImageView"
        contentView.accessibilityIdentifier = "profileContentView"

        NSLayoutConstraint.activate([
            contentLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
            contentLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 10),
            contentLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -10),
            
            userImageView.topAnchor.constraint(equalTo: contentLabel.bottomAnchor, constant: 10),
            userImageView.widthAnchor.constraint(equalToConstant: 25),
            userImageView.heightAnchor.constraint(equalToConstant: 25),
            userImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 10),
            userImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
            
            ])
        
    }
    
}
3.2.1 子控件實(shí)現(xiàn)
  • 為了提高代碼可讀性,這里建議大家使用懶加載的形式
  • 我們注意到,子控件的 view 是需要添加到 cellcontentView 上的,而非直接添加到 self 上
  • 另一點(diǎn)就是之前說(shuō)的,需要把 viewtranslatesAutoresizingMaskIntoConstraints 屬性設(shè)置為 false
  • 拿代碼 + 注釋舉個(gè)例子:
    lazy var contentLabel: UILabel = {
        let label = UILabel()
        // translatesAutoresizingMaskIntoConstraints 設(shè)為 false
        label.translatesAutoresizingMaskIntoConstraints = false
        // 添加到 contentView
        self.contentView.addSubview(label)
        label.numberOfLines = 0
        return label
    }()
3.2.2 計(jì)算子控件以及 cell 大小
  • 這里我們就不能再采用上古時(shí)代設(shè)定 frame 的方法而是通過(guò) NSLayoutConstraint.activate([...]) 中設(shè)定子控件各邊與 cell 各邊的關(guān)系來(lái)指定
  • 另外一點(diǎn)就是,對(duì)于每個(gè)子 view 以及我們 cellconteentView 我們都需要設(shè)定它們的 accessibilityIdentifier ,其內(nèi)容直接寫該 view 的名字就行,只要不重名就行,沒(méi)有太多的要求
  • 給大家舉個(gè)栗子 ?? :
    func setupConstraint() {
        // 
        userImageView.accessibilityIdentifier = "userImageView"
        contentLabel.accessibilityIdentifier = "profileImageView"
        contentView.accessibilityIdentifier = "profileContentView"
        // 設(shè)定子布局各邊與 cell 的關(guān)系
        NSLayoutConstraint.activate([
            contentLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
            contentLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 10),
            contentLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -10),
            
            userImageView.topAnchor.constraint(equalTo: contentLabel.bottomAnchor, constant: 10),
            userImageView.widthAnchor.constraint(equalToConstant: 25),
            userImageView.heightAnchor.constraint(equalToConstant: 25),
            userImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 10),
            userImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
            
            ])
        
    }

其中子 view 的大小,我們同樣可以在 activate([...]) 中,通過(guò) widthAnchor & heightAnchor 強(qiáng)制來(lái)設(shè)定

3.3 列表界面 - UITableViewController

  • 相比于 cell 中的注意點(diǎn),對(duì)于 UItableView 本身需要注意的地方并不多
  • 相比于普通 UItableView 的使用,這里要添加 translatesAutoresizingMaskIntoConstraints 的設(shè)置
  • 同時(shí)通過(guò) NSLayoutConstraint.activate(...) 設(shè)置設(shè)置 tableView 之于 ViewController 大小
import Foundation
import UIKit

class LandscapeListViewController: UITableViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        config()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        NSLayoutConstraint.activate(tableView.edgeConstraints(top: 80, left: 0, bottom: 0, right: 0))
    }
    
    func config() {
        tableView.delegate = self
        tableView.dataSource = self
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.estimatedRowHeight = 80
        tableView.rowHeight = UITableView.automaticDimension

        tableView.register(MemberCell.self, forCellReuseIdentifier: "MemberCell")
    }
    
}

extension LandscapeListViewController {
    
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return max(ContentProvider.datas.count, ContentProvider.imgs.count)
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "MemberCell", for: indexPath) as! MemberCell
        cell.contentLabel.text = ContentProvider.datas[indexPath.row]
        cell.userImageView.image = UIImage(systemName: ContentProvider.imgs[indexPath.row])
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
    }
    
}
3.3.1 自動(dòng)標(biāo)注尺寸
  • 首先我們需要設(shè)置 UITableView 的高度計(jì)算方案為自動(dòng)標(biāo)注尺寸,也就是 automaticDimension
  • 其次我們同樣需要標(biāo)注 tableviewtranslatesAutoresizingMaskIntoConstraintsfalse
    func config() {
        tableView.delegate = self
        tableView.dataSource = self
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.estimatedRowHeight = 80
        tableView.rowHeight = UITableView.automaticDimension

        tableView.register(MemberCell.self, forCellReuseIdentifier: "MemberCell")
    }
3.3.2 設(shè)定大小
  • TableViewCell 一樣,對(duì)于 viewController 中的 tableView ,我們也需要設(shè)定它們的大小關(guān)系
  • 為了方便起見(jiàn),我才用了網(wǎng)上的一套設(shè)定方案:
extension UIView {
    /// 設(shè)定 view 與其父 view 各邊之間的關(guān)系
    public func edgeConstraints(top: CGFloat, left: CGFloat, bottom: CGFloat, right: CGFloat) -> [NSLayoutConstraint] {
        return [
            self.leftAnchor.constraint(equalTo: self.superview!.leftAnchor, constant: left),
            self.rightAnchor.constraint(equalTo: self.superview!.rightAnchor, constant: -right),
            self.topAnchor.constraint(equalTo: self.superview!.topAnchor, constant: top),
            self.bottomAnchor.constraint(equalTo: self.superview!.bottomAnchor, constant: -bottom)
        ]
    }
}

  • 這樣一來(lái),帶代碼里我們只要通過(guò) view.edgeConstraints(...) 就可以快速設(shè)定子 view 與其 superview 之間的關(guān)系:
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // 調(diào)用封裝的方法:edgeConstraints()
        NSLayoutConstraint.activate(tableView.edgeConstraints(top: 80, left: 0, bottom: 0, right: 0))
    }

總結(jié)

  • 我在 GitHub 新建了一個(gè)倉(cāng)庫(kù),正在為大家整理、分享我的 iOS 學(xué)習(xí)筆記,歡迎大家 star 支持:https://github.com/Knowledge-Precipitation-Tribe/ios_notes
  • 如果大家有更好的方案,歡迎在評(píng)論區(qū)分享代碼,我會(huì)更新到本文中 ??
  • 同時(shí)歡迎大家點(diǎn)贊或者關(guān)注支持,因?yàn)檫@是我持續(xù)輸出的最大動(dòng)力~
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容