SnapKit 11: UITableView va Self-Sizing Cells — Dinamik, Moslashuvchan Hujayralar
🧩 SnapKit — Zero to Hero
SnapKit 11: UITableView va Self-Sizing Cells — SnapKit bilan Dinamik, Moslashuvchan Hujayralar
Holat: 2026-yil oktabr holatiga ko'ra, iOS ilovalarda ro'yxatlar (lists) haligacha eng muhim UI elementlardan biri hisoblanadi. Garchi SwiftUI o'zining List va ScrollView'lari bilan mashhur bo'lsa-da, katta va murakkab loyihalarda baribir
UITableView(vaUICollectionView) o'zining unumdorligi bilan ajralib turadi. SnapKit orqali dinamik balandlikka ega (self-sizing) hujayralarni yaratish har bir iOS dasturchi bilishi shart bo'lgan ko'nikmadir. Ushbu darsda biz murakkab hujayralarni qanday qilib to'g'ri chizishni va ishlash tezligini (performance) oshirishni o'rganamiz.
Mundarija
- UITableViewCell + SnapKit asoslari
- Self-Sizing Cells (Avtomatik balandlik) sirlari
- Amaliy loyiha 1: Contact List Cell
- Amaliy loyiha 2: Telegram Chat Bubble Cell
- Amaliy loyiha 3: Expandable Cell (Ochiluvchi hujayra)
- Cell Reuse (Qayta foydalanish) bilan bog'liq muammolar
- Header va Footer view'lar
- Performance Tips (Unumdorlik maslahatlari)
- Xulosa
- Manbalar
1. UITableViewCell + SnapKit asoslari {#basics}
UITableViewCell bilan ishlashda eng katta xatolardan biri — subview'larni to'g'ridan-to'g'ri self ga yoki self.view ga (bunday property o'zi yo'q) qo'shishdir.
Muhim: Har doim subview'larni
contentViewga qo'shing. Agar ularni to'g'ridan-to'g'ri cell'ga qo'shsangiz, editing rejimi (surib o'chirish kabi) yoki accessory view'lar ishlaganda xatoliklar chiqadi va layout buziladi.
import UIKit
import SnapKit
class BasicCell: UITableViewCell {
let titleLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupSubviews()
setupConstraints()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
private func setupSubviews() {
// XATO: self.addSubview(titleLabel)
// TO'G'RI:
contentView.addSubview(titleLabel)
}
private func setupConstraints() {
titleLabel.snp.makeConstraints { make in
// Constraint'lar ham contentView'ga bog'lanishi kerak
make.edges.equalToSuperview().inset(16)
}
}
}
2. Self-Sizing Cells (Avtomatik balandlik) sirlari {#self-sizing}
Cell o'z balandligini avtomatik hisoblashi uchun (Self-Sizing), sizga ikki narsa kerak:
UITableViewsozlamalari- "Uzilmas zanjir" (Unbroken chain of constraints)
TableView sozlamalari:
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 60 // Tahminiy balandlikni to'g'ri bering
Unbroken Chain of Constraints:
Cell o'z balandligini bilishi uchun, contentView.snp.top dan tortib to contentView.snp.bottom gacha constraint'lar uzluksiz bog'langan bo'lishi SHART. Agar zanjir qayerdadir uzilib qolsa, balandlik noto'g'ri hisoblanadi va odatda 44pt (iOS'dagi standart balandlik) bo'lib qoladi.
[contentView.top]
| (top offset: 16)
[titleLabel]
| (bottom offset: 8)
[subtitleLabel]
| (bottom offset: 16)
[contentView.bottom]
Bu kodda shunday yoziladi:
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(16) // Zanjir boshi
make.leading.trailing.equalToSuperview().inset(16)
}
subtitleLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(8) // Zanjir o'rtasi
make.leading.trailing.equalTo(titleLabel)
make.bottom.equalToSuperview().offset(-16) // Zanjir oxiri!
}
3. Amaliy loyiha 1: Contact List Cell {#project1}
Keling, oddiy kontaktlar ro'yxati hujayrasini yaratamiz. Unda avatar, ism, raqam va onlayn status bo'ladi.
import UIKit
import SnapKit
class ContactCell: UITableViewCell {
static let identifier = "ContactCell"
private let avatarImageView: UIImageView = {
let iv = UIImageView()
iv.backgroundColor = .systemGray5
iv.layer.cornerRadius = 24
iv.clipsToBounds = true
return iv
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .boldSystemFont(ofSize: 16)
return label
}()
private let phoneLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 14)
label.textColor = .systemGray
return label
}()
private let onlineIndicator: UIView = {
let view = UIView()
view.backgroundColor = .systemGreen
view.layer.cornerRadius = 5
return view
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupSubviews()
setupConstraints()
}
required init?(coder: NSCoder) { fatalError() }
private func setupSubviews() {
contentView.addSubview(avatarImageView)
contentView.addSubview(nameLabel)
contentView.addSubview(phoneLabel)
contentView.addSubview(onlineIndicator)
}
private func setupConstraints() {
avatarImageView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16)
make.centerY.equalToSuperview()
make.size.equalTo(CGSize(width: 48, height: 48))
// Zanjir yordamida minimum balandlikni ta'minlaymiz
make.top.bottom.equalToSuperview().inset(12).priority(.high)
}
nameLabel.snp.makeConstraints { make in
make.top.equalTo(avatarImageView.snp.top)
make.leading.equalTo(avatarImageView.snp.trailing).offset(12)
make.trailing.equalToSuperview().offset(-16)
}
phoneLabel.snp.makeConstraints { make in
make.bottom.equalTo(avatarImageView.snp.bottom)
make.leading.equalTo(nameLabel)
make.trailing.equalTo(nameLabel)
}
onlineIndicator.snp.makeConstraints { make in
make.size.equalTo(CGSize(width: 10, height: 10))
make.trailing.equalTo(avatarImageView.snp.trailing)
make.bottom.equalTo(avatarImageView.snp.bottom)
}
}
func configure(name: String, phone: String, isOnline: Bool) {
nameLabel.text = name
phoneLabel.text = phone
onlineIndicator.isHidden = !isOnline
}
}
4. Amaliy loyiha 2: Telegram Chat Bubble Cell {#project2}
Chat ilovalari hujayralari dinamik balandlik uchun mukammal misoldir. Matn uzunligiga qarab balandlik o'zgarishi kerak va xabarlar o'ngda (outgoing) yoki chapda (incoming) turishi kerak.
Senior eslatmasi: Bitta cell ishlatib, uni incoming/outgoing'ga qarab snapkit bilan o'ngga/chapga surishdan ko'ra, ko'pincha
remakeConstraintsishlatiladi yoki alohidaIncomingCellvaOutgoingCellyaratgan ma'qul. Lekin o'rganish maqsadida biz constraint'larniremakeqilamiz.
import UIKit
import SnapKit
class ChatBubbleCell: UITableViewCell {
static let identifier = "ChatBubbleCell"
private let bubbleView: UIView = {
let view = UIView()
view.layer.cornerRadius = 16
return view
}()
private let messageLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0 // Matn ko'p qatorli bo'lishi mumkin!
label.font = .systemFont(ofSize: 16)
return label
}()
private let timeLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 11)
label.textColor = .systemGray
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = .clear
selectionStyle = .none
contentView.addSubview(bubbleView)
bubbleView.addSubview(messageLabel)
bubbleView.addSubview(timeLabel)
// Elementlar ichidagi joylashuv har doim bir xil:
messageLabel.snp.makeConstraints { make in
make.top.leading.equalToSuperview().offset(12)
make.trailing.equalToSuperview().offset(-12)
}
timeLabel.snp.makeConstraints { make in
make.top.equalTo(messageLabel.snp.bottom).offset(4)
make.trailing.bottom.equalToSuperview().offset(-8)
}
}
required init?(coder: NSCoder) { fatalError() }
func configure(message: String, time: String, isOutgoing: Bool) {
messageLabel.text = message
timeLabel.text = time
bubbleView.backgroundColor = isOutgoing ? .systemBlue : .systemGray5
messageLabel.textColor = isOutgoing ? .white : .black
timeLabel.textColor = isOutgoing ? .systemGray5 : .systemGray
// Asosiy SnapKit mo'jizasi shu yerda:
bubbleView.snp.remakeConstraints { make in
make.top.equalToSuperview().offset(8)
make.bottom.equalToSuperview().offset(-8)
make.width.lessThanOrEqualTo(contentView).multipliedBy(0.75) // Bubble eng ko'pi ekran 75% ni egallaydi
if isOutgoing {
make.trailing.equalToSuperview().offset(-16)
} else {
make.leading.equalToSuperview().offset(16)
}
}
}
}
5. Amaliy loyiha 3: Expandable Cell (Ochiluvchi hujayra) {#project3}
Ba'zida hujayra ustiga bosilganda qo'shimcha ma'lumot ochilishi kerak bo'ladi. Buni amalga oshirish juda oson, faqat to'g'ri arxitektura va animatsiya kerak.
import UIKit
import SnapKit
class ExpandableCell: UITableViewCell {
static let identifier = "ExpandableCell"
private let titleLabel: UILabel = {
let label = UILabel()
label.font = .boldSystemFont(ofSize: 16)
return label
}()
private let detailLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.textColor = .darkGray
return label
}()
// Bottom constraint'ga referens saqlaymiz
private var detailBottomConstraint: Constraint?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(titleLabel)
contentView.addSubview(detailLabel)
titleLabel.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview().inset(16)
}
detailLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(8)
make.leading.trailing.equalTo(titleLabel)
// Buni saqlaymiz
self.detailBottomConstraint = make.bottom.equalToSuperview().offset(-16).constraint
}
}
required init?(coder: NSCoder) { fatalError() }
func configure(title: String, detail: String, isExpanded: Bool) {
titleLabel.text = title
detailLabel.text = detail
if isExpanded {
detailLabel.isHidden = false
detailLabel.alpha = 1
detailBottomConstraint?.update(offset: -16)
} else {
detailLabel.isHidden = true
detailLabel.alpha = 0
detailBottomConstraint?.update(offset: 0) // Paddingni nol qilamiz
}
}
}
Controller ichida ishlatilishi (Animatsiya siri):
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// isExpanded qiymatini o'zgartiring
// dataSource[indexPath.row].isExpanded.toggle()
// Cell'ni qayta hisoblash uchun mo'jizaviy kod:
tableView.beginUpdates()
tableView.endUpdates()
}
6. Cell Reuse (Qayta foydalanish) bilan bog'liq muammolar {#reuse}
UITableView hujayralarni qayta ishlatadi. Agar siz oldingi hujayra holatini tozalamasangiz, eski constraint'lar qolib ketishi va log'larda xatolar kelib chiqishi mumkin.
- **
prepareForReuse()ni qo'llash:**
override func prepareForReuse() {
super.prepareForReuse()
// Rasmlarni tozalang, tekstlarni null qiling
avatarImageView.image = nil
// Agar elementlarni isHidden qilib boshqarsangiz, standart holatga qaytaring
onlineIndicator.isHidden = true
}
- **
remakeConstraintsvs Update:**
Agar hujayra strukturasini butunlay o'zgartirmoqchi bo'lsangiz remakeConstraints ishlatasiz. Ammo unutmang, bu biroz resurs talab qiladi. Yaxshisi, eng kerakli Constraint larni reference (havola) sifatida o'zgaruvchida saqlab, faqato'sha yerga .update(offset:) qo'llagan ma'qul.
7. Header va Footer view'lar {#header-footer}
Odatda TableView'ga header qo'shish qiyindek tuyuladi, chunki tableHeaderView avtomatik o'z balandligini self-size qilmaydi. Sizga systemLayoutSizeFitting "trick" yordam beradi.
class MyHeaderView: UIView {
let titleLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(titleLabel)
titleLabel.numberOfLines = 0
titleLabel.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(20)
}
}
required init?(coder: NSCoder) { fatalError() }
}
// Controller'da:
let header = MyHeaderView()
header.titleLabel.text = "Ushbu uzun matn dinamik o'lchamlarga ega bo'ladi. SnapKit bu yerda ham yordamga keladi!"
// Balandlikni hisoblash:
let targetSize = CGSize(width: tableView.bounds.width, height: UIView.layoutFittingCompressedSize.height)
let size = header.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
header.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
tableView.tableHeaderView = header
Section Header/Footer uchun esa UITableViewHeaderFooterView vorislarini xuddi cell'dek yaratib, contentView ga bog'laysiz, ularda avtomatik self-sizing ishlayveradi.
8. Performance Tips (Unumdorlik maslahatlari) {#performance}
- **To'g'ri
estimatedRowHeight:** Agar estimatedRowHeight qiymati real qiymatdan juda katta farq qilsa, sahifani aylantirganda (scroll) scrollbar sakrab ketadi va lag kuzatiladi. Iloji boricha aniqroq o'rtacha balandlikni bering. - **
shouldRasterize:** Agar hujayrangizda juda ko'p subview'lar va constraint'lar bo'lsa (masalan, feed postlari), layer rasterization ishlating:
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
- **
layoutIfNeeded()dan qoching:** Cell ichidagiconfigure()funksiyasida yokisetupConstraintsdaself.layoutIfNeeded()ni asossiz chaqirmang. Bu ko'p marta qayta hisoblashga va frame-drop (qotish) ga olib keladi.
Xulosa {#xulosa}
SnapKit yordamida UITableViewCell'lar yasash an'anaviy Storyboard/XIB'dan ancha qulay, tezkor hamda xavfsizroq (merge conflict'lar yo'q). Eng muhimi: doimo elementlarni contentView'ga qo'shishni va "zanjir"ni (top dan bottom gacha) uzib qo'ymaslikni unutmang. Ushbu darsdagi uchta amaliy misolni o'z loyihangizda qayta yozib chiqsangiz, SnapKit bilan cell'lar yaratish ikkinchi tabiatingizga aylanadi.