Linh Tạ

Dynamic height UITableView

Lưu ý: Bài viết này không nói về cách tự động resize UITableViewCell mà là về cách khiến bản thân UITableView tự động thay đổi chiều cao dựa theo nội dung của nó.

Trong quá trình làm việc mình thường xuyên gặp trường hợp mà việc UITableView tự động resize sẽ khiến công việc nhẹ nhàng hơn rất nhiều, đặc biệt khi bạn muốn show một danh sách các lựa chọn như hình dưới đây.

Dynamic height UITableView example

Hai cách thông dụng để xử lí trường hợp trên là tự thay frame của UITableView hoặc kéo height constraint của UITableView rồi set constant bằng code. Cả hai cách làm trên đều thủ công và rắc rối hơn mức cần thiết.

Tự động resize UITableView

Biến contentSize của UITableView là size chính xác của chiều rộng UITableView và tổng chiều cao của nội dung bên trong nó. contentSize được hệ thống tính toán một cách tự động mỗi khi data của UITableView thay đổi nên bạn có thể yên tâm về độ chính xác và bỏ qua các bước tính toán thủ công.

Tuy nhiên contentSize thôi là chưa đủ vì nó chỉ đại diện cho chiều cao của nội dung trong UITableView chứ không đại diện cho chiều cao của UITableView. Để đồng bộ giá trị này với chiều cao thực tế của UITableView, ta cần sử dụng intrinsicContentSize.

intrinsicContentSize

Một vài UIView có size tự nhiên dựa theo nội dung của nó. Các ví dụ có thể kể đến như UIButton, UILabel (với numberOfLines khác 0), hoặc bất kì view nào trong file giao diện mà không bị báo lỗi khi thiếu constraint width và height. Size tự nhiên này chính là intrinsicContentSize. Đối với những view không có size tự nhiên như UITableView, giá trị này sẽ bằng (-1, -1).

Với hiểu biết về intrinsicContentSize ta đã có thể giúp UITableView tự động thay đổi chiều cao mà không cần set frame hay set height constraint cho nó. Tất cả những gì chúng ta phải làm là subclass SelfSizingTableView và cập nhật intrinsicContentSize mỗi khi contentSize của UITableView thay đổi.


class SelfSizingTableView: UITableView {
    var maxHeight: CGFloat = .infinity
    
    override var contentSize: CGSize {
        didSet {
            invalidateIntrinsicContentSize()
            setNeedsLayout()
        }
    }
    
    override var intrinsicContentSize: CGSize {
        let height = min(maxHeight, contentSize.height)
        return CGSize(width: contentSize.width, height: height)
    }
}

Bất kì khi nào contentSize được set, hàm invalidateIntrinsicContentSize sẽ được gọi để huỷ intrinsicContentSize cũ. Ngay sau đó ta khiến hệ thống cập nhật lại layout mới nhất bằng cách gọi setNeedsLayout. Trong trường hợp cần giới hạn chiều cao của UITableView bạn có thể set lại giá trị maxHeight. Sau khi đạt đến giá trị tối đa cho phép, UITableView sẽ scroll như bình thường.

Dùng SelfSizingTableView trong file giao diện

Để UITableView tự động resize, bạn không được đặt constraint chiều cao hay fix cứng top của SelfSizingTableView. Tuy nhiên việc setup không đủ constraint này sẽ bị Xcode báo lỗi.

Problem with dynamic height UITableView when working with UI files

Bình thường UITableView sẽ không được hiển thị chính xác nếu thiếu constraint nhưng trong trường hợp này ta biết rằng UITableView chắc chắn sẽ có chiều cao - intrinsicContentSize khác (-1, -1) nên ta có thể tắt warning bằng cách đổi Intrinsic Size trong file giao diện từ Default sang Placeholder.

Use placeholder to get rid of XCode error

SelfSizingTableView hoạt động tốt với cả cell có chiều cao cố định lẫn dynamic nên bạn có thể yên tâm sử dụng. Nếu có bất kì thắc mắc hay góp ý gì đừng ngại gửi mail cho mình nhé.