Azimjon Abdurasulov

UIKit 13: Zamonaviy UICollectionView — Compositional Layout, Diffable Data Source va Cell Registration

📱 UIKit va SwiftUI — chuqur

UIKit 13: Zamonaviy UICollectionView — Compositional Layout, Diffable Data Source va Cell Registration

Holat: 2026-yil sentabr. Eski UICollectionViewFlowLayout, UICollectionViewDataSource va reloadData() davri butunlay o'tmishda qoldi. Ilgari har bir dasturchi numberOfItemsInSection va cellForItemAt metodlarida index path'larni noto'g'ri hisoblab, mashhur NSInternalInconsistencyException (Invalid update: invalid number of items) crash'iga duch kelardi. Zamonaviy iOS dasturlashda 3 ta ustun yaratilgan: Compositional Layout, Diffable Data Source va Cell Registration. Ushbu darsda App Store darajasidagi murakkab, ko'p qatlamli va 120 FPS'da silliq ishlovchi interfeyslarni noldan qurish mexanikasi tahlil qilinadi.

Mundarija

  1. Eski vs Zamonaviy Collection View: Nega inqilob yuz berdi?
  2. UICollectionViewCompositionalLayout: 4 pog'onali mental model
  3. Orthogonal Scrolling: App Store uslubidagi gorizontal karusellar
  4. Diffable Data Source & Snapshots: Myers diffing algoritmi
  5. Zamonaviy Cell Registration: String identifikatorlarsiz xavfsiz kod
  6. To'liq Amaliy Loyiha: App Store Feed ekrani (Banner + Grid + List)
  7. Senior Interview Savollari
  8. Xulosa
  9. Manbalar

1. Eski vs Zamonaviy Collection View: Nega inqilob yuz berdi? {#evolution}

Ilgari bir ekranda ham gorizontal karusel, ham vertikal ro'yxat, ham 2 ustunli setka (grid) qilish uchun dasturchilar UITableViewCell ichiga alohida UICollectionView tiqishga (nested collection views) majbur bo'lishardi. Bu esa xotirani yeb, scroll paytida kuchli qotishlarni (scroll stutter) keltirib chiqarardi.

XususiyatEski usul (Legacy UIKit)Zamonaviy usul (Modern UIKit)
LayoutUICollectionViewFlowLayout (Cheklangan, faqat bitta yo'nalish)UICollectionViewCompositionalLayout (Item -> Group -> Section)
Gorizontal scrollSubview ichiga yangi CollectionView qo'yishorthogonalScrollingBehavior (Bitta satr kod!)
Data SourceUICollectionViewDataSource (Index path asosida, xavfli)UICollectionViewDiffableDataSource<Section, Item> (Xavfsiz)
Ma'lumot yangilashreloadData() (O'chib-yonish) yoki performBatchUpdates (Crash xavfi)apply(snapshot, animatingDifferences: true) (Myers diffing O(N))
Cell ro'yxatdan o'tkazishregister(MyCell.self, forCellWithReuseIdentifier: "MyCell")UICollectionView.CellRegistration<Cell, Item> (Generik & Type-Safe)

2. UICollectionViewCompositionalLayout: 4 pog'onali mental model {#compositional-layout}

Compositional Layout o'lchamlarni hisoblashda legoga o'xshaydi: kichik g'ishtchalardan yirik inshoot yasaladi.

+───────────────────────────────────────────────────────────────+
| Layout (UICollectionViewCompositionalLayout)                  |
|  +─────────────────────────────────────────────────────────+  |
|  | Section 1 (NSCollectionLayoutSection)                   |  |
|  |   +───────────────────────────────────────────────────+ |  |
|  |   | Group (NSCollectionLayoutGroup)                   | |  |
|  |   |   +──────────────+  +──────────────+              | |  |
|  |   |   | Item 1       |  | Item 2       |              | |  |
|  |   |   | (Item)       |  | (Item)       |              | |  |
|  |   |   +──────────────+  +──────────────+              | |  |
|  |   +───────────────────────────────────────────────────+ |  |
|  +─────────────────────────────────────────────────────────+  |
+───────────────────────────────────────────────────────────────+

O'lcham turlari (NSCollectionLayoutDimension):

  1. **.fractionalWidth(0.5) / .fractionalHeight(1.0):** Ota element o'lchamining foizi (masalan, 0.5 = 50%).
  2. **.absolute(100):** Aniq nuqtalarda berilgan qat'iy o'lcham (masalan, aynan 100 pt).
  3. **.estimated(60):** O'zgaruvchan (dinamik) o'lcham. Ichidagi kontentga (masalan, ko'p qatorli matnga) qarab o'zi kengayadi.
import UIKit

@MainActor
func createTwoColumnGridSection() -> NSCollectionLayoutSection {
    // 1. Item: Ota guruh enining 50%i, balandligi esa guruh balandligi bilan teng:
    let itemSize = NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(0.5),
        heightDimension: .fractionalHeight(1.0)
    )
    let item = NSCollectionLayoutItem(layoutSize: itemSize)
    item.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)

    // 2. Group: Ekran enining 100%i, balandligi qat'iy 120 pt:
    let groupSize = NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: .absolute(120)
    )
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, repeatingSubitem: item, count: 2)

    // 3. Section: Guruhlar ketma-ketligi:
    let section = NSCollectionLayoutSection(group: group)
    section.contentInsets = NSDirectionalEdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)
    return section
}

3. Orthogonal Scrolling: App Store uslubidagi gorizontal karusellar {#orthogonal-scrolling}

Vertikal aylanuvchi ro'yxat ichida alohida bir bo'limni gorizontal aylanuvchi karuselga aylantirish uchun endi nested collection view kerak emas! Bor-yo'g'i bitta xususiyat o'rnatiladi:

import UIKit

@MainActor
func createBannerCarouselSection() -> NSCollectionLayoutSection {
    let itemSize = NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: .fractionalHeight(1.0)
    )
    let item = NSCollectionLayoutItem(layoutSize: itemSize)

    let groupSize = NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(0.85), // Kartaning cheti ko'rinib tursin
        heightDimension: .absolute(180)
    )
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

    let section = NSCollectionLayoutSection(group: group)
    section.interGroupSpacing = 16
    section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 16, bottom: 20, trailing: 16)

    // SEHR: Bir satr kod bilan gorizontal silliq karusel:
    section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary

    return section
}

orthogonalScrollingBehavior variantlari:

  • **.none:** Standart vertikal harakat.
  • **.continuous:** Oddiy erkin gorizontal scroll.
  • **.continuousGroupLeadingBoundary:** (App Store standarti) Scroll to'xtaganda eng yaqin guruhning chap chegarasiga avtomatik qadalanadi (magnetic snap).
  • **.paging:** Butun sahifa bo'lib o'tadi.

4. Diffable Data Source & Snapshots: Myers diffing algoritmi {#diffable-data-source}

Eski UICollectionViewDataSourceda ma'lumotlar massivini o'zgartirib reloadData() chaqirilsa, barcha ko'rinib turgan kataklar ekrandan o'chib, qaytadan chizilardi — natijada ekranda o'chib-yonish (flickering) bo'lardi. performBatchUpdates esa elementlar indeksini 1 taga adashtirsangiz ham butun ilovani crash qilardi.

**UICollectionViewDiffableDataSource** bu muammoni butunlay hal qildi:

  1. Siz ma'lumotlar massiviga emas, **NSDiffableDataSourceSnapshot** obyektiga tayanasiz.
  2. Snapshot ichiga bo'limlar va ularning elementlarini solasiz.
  3. apply(snapshot, animatingDifferences: true) chaqirilganda, Apple'ning ichki Myers Diffing algoritmi (O(N)) eski snapshot bilan yangi snapshot o'rtasidagi farqni topadi.
  4. Faqat qo'shilgan, o'chirilgan yoki surilgan kataklar xirurgik aniqlikda, mukammal animatsiya bilan o'zgaradi!
[ Eski Snapshot ]  ──────── Myers Diffing (O(N)) ────────► [ Yangi Snapshot ]
  Item A                     Faqat o'zgargan Item C           Item A
  Item B ── (O'chirildi)      uchirilib, Item D qo'shiladi    Item C
  Item C                                                      Item D (Yangi!)

Qoida: Snapshot'dagi SectionIdentifierType va ItemIdentifierType albatta **Hashable va Sendable** bo'lishi shart!


5. Zamonaviy Cell Registration: String identifikatorlarsiz xavfsiz kod {#cell-registration}

Ilgari kataklarni string identifikatorlar bilan ro'yxatdan o'tkazib, keyin majburiy kasting (as! MyCell) qilardik: collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath) as! MyCell Bitta harf xato ketsa, dastur darhol crash bo'lardi.

iOS 14+ da paydo bo'lgan **UICollectionView.CellRegistration** generik bo'lib, xavfsizlikni 100% kafolatlaydi:

import UIKit

struct ProductItem: Hashable, Sendable {
    let id: UUID
    let title: String
    let price: String
}

@MainActor
func makeProductCellRegistration() -> UICollectionView.CellRegistration<UICollectionViewListCell, ProductItem> {
    UICollectionView.CellRegistration<UICollectionViewListCell, ProductItem> { cell, indexPath, item in
        var content = cell.defaultContentConfiguration()
        content.text = item.title
        content.secondaryText = item.price
        content.image = UIImage(systemName: "cart.fill")
        cell.contentConfiguration = content
    }
}

6. To'liq Amaliy Loyiha: App Store Feed ekrani (Banner + Grid + List) {#example}

Quyida barcha 3 ta zamonaviy texnologiyani birlashtirgan, to'liq ishlovchi feed ekrani:

import UIKit

// 1. Modellar (Hashable va Sendable bo'lishi shart)
public enum FeedSection: Int, CaseIterable, Hashable, Sendable {
    case banners
    case categories
    case topApps
}

public struct FeedItem: Hashable, Sendable {
    public let id: String
    public let title: String
    public let subtitle: String
    public let iconName: String

    public init(id: String, title: String, subtitle: String, iconName: String) {
        self.id = id
        self.title = title
        self.subtitle = subtitle
        self.iconName = iconName
    }
}

// 2. Asosiy ViewController
public final class AppStoreFeedViewController: UIViewController {

    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<FeedSection, FeedItem>!

    public override func viewDidLoad() {
        super.viewDidLoad()
        title = "Discover"
        view.backgroundColor = .systemBackground

        configureCollectionView()
        configureDataSource()
        loadInitialData()
    }

    private func configureCollectionView() {
        collectionView = UICollectionView(
            frame: view.bounds,
            collectionViewLayout: createLayout()
        )
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.backgroundColor = .systemBackground
        view.addSubview(collectionView)
    }

    // 3. Murakkab 3 xil bo'limli Compositional Layout
    private func createLayout() -> UICollectionViewLayout {
        UICollectionViewCompositionalLayout { [weak self] sectionIndex, layoutEnvironment in
            guard let self, let section = FeedSection(rawValue: sectionIndex) else { return nil }

            switch section {
            case .banners:
                return self.createBannerSection()
            case .categories:
                return self.createCategoryGridSection()
            case .topApps:
                return self.createTopAppsListSection(layoutEnvironment: layoutEnvironment)
            }
        }
    }

    private func createBannerSection() -> NSCollectionLayoutSection {
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1.0),
            heightDimension: .fractionalHeight(1.0)
        )
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        let groupSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(0.88),
            heightDimension: .absolute(190)
        )
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

        let section = NSCollectionLayoutSection(group: group)
        section.interGroupSpacing = 14
        section.contentInsets = NSDirectionalEdgeInsets(top: 12, leading: 16, bottom: 20, trailing: 16)
        // Karusel effekti
        section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
        return section
    }

    private func createCategoryGridSection() -> NSCollectionLayoutSection {
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(0.5),
            heightDimension: .fractionalHeight(1.0)
        )
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        item.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)

        let groupSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1.0),
            heightDimension: .absolute(60)
        )
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, repeatingSubitem: item, count: 2)

        let section = NSCollectionLayoutSection(group: group)
        section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 12, bottom: 20, trailing: 12)
        return section
    }

    private func createTopAppsListSection(
        layoutEnvironment: NSCollectionLayoutEnvironment
    ) -> NSCollectionLayoutSection {
        var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        config.headerMode = .none
        return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment)
    }

    // 4. Type-safe Cell Registration va Diffable Data Source
    private func configureDataSource() {
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, FeedItem> {
            cell, indexPath, item in
            var content = cell.defaultContentConfiguration()
            content.text = item.title
            content.secondaryText = item.subtitle
            content.image = UIImage(systemName: item.iconName)
            cell.contentConfiguration = content
        }

        dataSource = UICollectionViewDiffableDataSource<FeedSection, FeedItem>(
            collectionView: collectionView
        ) { collectionView, indexPath, item in
            collectionView.dequeueConfiguredReusableCell(
                using: cellRegistration,
                for: indexPath,
                item: item
            )
        }
    }

    // 5. Snapshot yuklash
    private func loadInitialData() {
        var snapshot = NSDiffableDataSourceSnapshot<FeedSection, FeedItem>()
        snapshot.appendSections([.banners, .categories, .topApps])

        snapshot.appendItems([
            FeedItem(id: "b1", title: "Yilning eng yaxshi ilovalari", subtitle: "2026 tanlovi", iconName: "star.fill"),
            FeedItem(id: "b2", title: "Apple Design Awards", subtitle: "G'oliblar ro'yxati", iconName: "trophy.fill")
        ], toSection: .banners)

        snapshot.appendItems([
            FeedItem(id: "c1", title: "Ta'lim", subtitle: "Kitoblar va fan", iconName: "book.fill"),
            FeedItem(id: "c2", title: "Moliya", subtitle: "Bank va investitsiya", iconName: "banknote.fill")
        ], toSection: .categories)

        snapshot.appendItems([
            FeedItem(id: "a1", title: "SwiftKey AI", subtitle: "Aqlli klaviatura", iconName: "keyboard"),
            FeedItem(id: "a2", title: "Focus Master", subtitle: "Vaqtni rejalashtirish", iconName: "clock.badge.checkmark")
        ], toSection: .topApps)

        dataSource.apply(snapshot, animatingDifferences: false)
    }
}

7. Senior Interview Savollari {#interview}

1. Myers Diffing algoritmi qanday ishlaydi va uning murakkabligi qanday?

Javob: Myers Diffing algoritmi ikkita ketma-ketlik (eski va yangi snapshot) o'rtasidagi eng qisqa tahrir masofasini (Shortest Edit Distance) topadi. Uning o'rtacha vaqt murakkabligi $O(N + D^2)$ bo'lib (bu yerda $N$ — elementlar soni, $D$ — farqlar soni), juda tez hisoblanadi va foydalanuvchiga freym yo'qotishlarsiz (hitch-free) silliq animatsiyani taqdim etadi.

2. Nega DiffableDataSource bilan reloadData() ishlatilmaydi?

Javob: reloadData() barcha ko'rinib turgan kataklarni darhol yo'q qilib, ularni noldan yaratadi. Bu esa xotirani ortiqcha band qiladi, layout o'lchamlarini qayta hisoblashga majbur qiladi va ekranning miltillashiga (flickering) sabab bo'ladi. Diffable DataSource esa apply() orqali faqat o'zgargan kataklargina yangilanishini ta'minlaydi.

3. CellRegistration ning eski identifikatorli ro'yxatdan o'tkazishdan asosiy ustunligi nimada?

Javob:

  1. Type-Safety: Majburiy string identifikatorlar va as! MyCell kastingiga chek qo'yildi.
  2. Kapsulatsiya: Katakni sozlash (configuration logic) bitta blok ichida saqlanadi, bu kodni modulli va oson testlanadigan qiladi.
  3. Xotira tejash: Keraksiz nib yuklash yoki xato identifikator bilan crash bo'lish xavfi nolga tushirilgan.

8. Xulosa {#xulosa}

  • UICollectionViewCompositionalLayout — o'lchamlarni deklarativ ravishda Item -> Group -> Section -> Layout pog'onalari bo'yicha belgilash imkonini beradi.
  • Bitta bo'limni gorizontal aylanuvchi qilish uchun **orthogonalScrollingBehavior** ning o'zi yetarli.
  • **DiffableDataSource va Snapshots** orqali NSInternalInconsistencyException crash'lari butunlay unutiladi.
  • **CellRegistration** orqali qat'iy tiplangan va xavfsiz kataklar yaratiladi.

9. Manbalar {#manbalar}

Markdown manba

iOS Lessons