# 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](#basics)
2. [Self-Sizing Cells (Avtomatik balandlik) sirlari](#self-sizing)
3. [Amaliy loyiha 1: Contact List Cell](#project1)
4. [Amaliy loyiha 2: Telegram Chat Bubble Cell](#project2)
5. [Amaliy loyiha 3: Expandable Cell (Ochiluvchi hujayra)](#project3)
6. [Cell Reuse (Qayta foydalanish) bilan bog'liq muammolar](#reuse)
7. [Header va Footer view'lar](#header-footer)
8. [Performance Tips (Unumdorlik maslahatlari)](#performance)
9. [Xulosa](#xulosa)
10. [Manbalar](#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.

```swift
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:
```swift
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.

```text
[contentView.top]
       | (top offset: 16)
  [titleLabel]
       | (bottom offset: 8)
[subtitleLabel]
       | (bottom offset: 16)
[contentView.bottom]
```

Bu kodda shunday yoziladi:
```swift
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.

```swift
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.

```swift
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.

```swift
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):**
```swift
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:**
```swift
override func prepareForReuse() {
    super.prepareForReuse()
    // Rasmlarni tozalang, tekstlarni null qiling
    avatarImageView.image = nil
    
    // Agar elementlarni isHidden qilib boshqarsangiz, standart holatga qaytaring
    onlineIndicator.isHidden = true 
}
```

2. **`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.

```swift
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:
   ```swift
   layer.shouldRasterize = true
   layer.rasterizationScale = UIScreen.main.scale
   ```
3. **`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}
- [SnapKit Rasmiy GitHub Repozitoriysi](https://github.com/SnapKit/SnapKit)
- [Apple Documentation: Table View Cells](https://developer.apple.com/documentation/uikit/uitableviewcell)
- [Self-Sizing Table View Cells xulosa](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithSelf-SizingTableViewCells.html)
