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,UICollectionViewDataSourcevareloadData()davri butunlay o'tmishda qoldi. Ilgari har bir dasturchinumberOfItemsInSectionvacellForItemAtmetodlarida index path'larni noto'g'ri hisoblab, mashhurNSInternalInconsistencyException(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
- Eski vs Zamonaviy Collection View: Nega inqilob yuz berdi?
- UICollectionViewCompositionalLayout: 4 pog'onali mental model
- Orthogonal Scrolling: App Store uslubidagi gorizontal karusellar
- Diffable Data Source & Snapshots: Myers diffing algoritmi
- Zamonaviy Cell Registration: String identifikatorlarsiz xavfsiz kod
- To'liq Amaliy Loyiha: App Store Feed ekrani (Banner + Grid + List)
- Senior Interview Savollari
- Xulosa
- 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.
| Xususiyat | Eski usul (Legacy UIKit) | Zamonaviy usul (Modern UIKit) |
|---|---|---|
| Layout | UICollectionViewFlowLayout (Cheklangan, faqat bitta yo'nalish) | UICollectionViewCompositionalLayout (Item -> Group -> Section) |
| Gorizontal scroll | Subview ichiga yangi CollectionView qo'yish | orthogonalScrollingBehavior (Bitta satr kod!) |
| Data Source | UICollectionViewDataSource (Index path asosida, xavfli) | UICollectionViewDiffableDataSource<Section, Item> (Xavfsiz) |
| Ma'lumot yangilash | reloadData() (O'chib-yonish) yoki performBatchUpdates (Crash xavfi) | apply(snapshot, animatingDifferences: true) (Myers diffing O(N)) |
| Cell ro'yxatdan o'tkazish | register(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):
- **
.fractionalWidth(0.5)/.fractionalHeight(1.0):** Ota element o'lchamining foizi (masalan, 0.5 = 50%). - **
.absolute(100):** Aniq nuqtalarda berilgan qat'iy o'lcham (masalan, aynan 100 pt). - **
.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:
- Siz ma'lumotlar massiviga emas, **
NSDiffableDataSourceSnapshot** obyektiga tayanasiz. - Snapshot ichiga bo'limlar va ularning elementlarini solasiz.
apply(snapshot, animatingDifferences: true)chaqirilganda, Apple'ning ichki Myers Diffing algoritmi (O(N)) eski snapshot bilan yangi snapshot o'rtasidagi farqni topadi.- 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
SectionIdentifierTypevaItemIdentifierTypealbatta **HashablevaSendable** 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:
- Type-Safety: Majburiy string identifikatorlar va
as! MyCellkastingiga chek qo'yildi. - Kapsulatsiya: Katakni sozlash (configuration logic) bitta blok ichida saqlanadi, bu kodni modulli va oson testlanadigan qiladi.
- 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. - **
DiffableDataSourcevaSnapshots** orqaliNSInternalInconsistencyExceptioncrash'lari butunlay unutiladi. - **
CellRegistration** orqali qat'iy tiplangan va xavfsiz kataklar yaratiladi.