Azimjon Abdurasulov

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 (va UICollectionView) 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

  1. UITableViewCell + SnapKit asoslari
  2. Self-Sizing Cells (Avtomatik balandlik) sirlari
  3. Amaliy loyiha 1: Contact List Cell
  4. Amaliy loyiha 2: Telegram Chat Bubble Cell
  5. Amaliy loyiha 3: Expandable Cell (Ochiluvchi hujayra)
  6. Cell Reuse (Qayta foydalanish) bilan bog'liq muammolar
  7. Header va Footer view'lar
  8. Performance Tips (Unumdorlik maslahatlari)
  9. Xulosa
  10. 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 contentView ga 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:

  1. UITableView sozlamalari
  2. "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 remakeConstraints ishlatiladi yoki alohida IncomingCell va OutgoingCell yaratgan ma'qul. Lekin o'rganish maqsadida biz constraint'larni remake qilamiz.

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.

  1. **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 
}
  1. **remakeConstraints vs 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}

  1. **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.
  2. **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
  1. **layoutIfNeeded() dan qoching:** Cell ichidagi configure() funksiyasida yoki setupConstraints da self.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.

Manbalar {#manbalar}

Markdown manba

iOS Lessons