SnapKit 12: UICollectionView + SnapKit — App Store Uslubidagi Vertikal + Gorizontal Layout
🧩 SnapKit — Zero to Hero
SnapKit 12: UICollectionView + SnapKit — App Store Uslubidagi Vertikal + Gorizontal Layout
Holat: 2026-yil oktabr. Ilovalar dizaynida eng ko'p so'raladigan va ko'p qo'llaniladigan tuzilma — bu App Store uslubidagi aralash scroll qilinadigan (vertikal va gorizontal) layoutlardir. Ushbu darsda biz SnapKit va
UICollectionViewCompositionalLayoutyordamida aynan shunday zamonaviy, murakkab va moslashuvchan dizaynlarni qanday yaratishni o'rganamiz.
Mundarija
- UICollectionView + SnapKit asoslari
- UICollectionViewFlowLayout + SnapKit Cell
- UICollectionViewCompositionalLayout (Zamonaviy usul)
- 🔥 APP STORE LAYOUT (Asosiy qism)
- Nested Horizontal ScrollView yondashuvi (Eski usul)
- Section Background Decoration
- Performance va Best Practices
- Xulosa
- Manbalar
1. UICollectionView + SnapKit asoslari {#basics}
UICollectionView — bu iOS'da ma'lumotlarni turli xil grid (to'r) va murakkab ko'rinishlarda taqdim etish uchun eng kuchli vositadir. SnapKit yordamida CollectionView'ni ekranga joylashtirish juda oson, biroq asosiy ish UICollectionViewCell ichidagi elementlarni to'g'ri joylashtirishda.
CollectionView'ni superview'ga yopishtirish
Odatda CollectionView butun ekranni egallaydi.
import UIKit
import SnapKit
class BasicCollectionViewController: UIViewController {
private lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .systemBackground
cv.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
return cv
}()
override func viewDidLoad() {
super.viewDidLoad()
setupSubviews()
setupConstraints()
}
private func setupSubviews() {
view.addSubview(collectionView)
}
private func setupConstraints() {
collectionView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
}
Cell ichida SnapKit bilan ishlash
Cell'ni loyihalashda eng muhim qoida: barcha subview'larni to'g'ridan-to'g'ri cell ga emas, balki contentView ga qo'shishingiz kerak!
import UIKit
import SnapKit
class BasicCell: UICollectionViewCell {
private let titleLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
setupConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupSubviews() {
// MUHIM: contentView'ga qo'shiladi!
contentView.addSubview(titleLabel)
contentView.backgroundColor = .systemBlue
contentView.layer.cornerRadius = 8
}
private func setupConstraints() {
titleLabel.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(8)
}
}
}
2. UICollectionViewFlowLayout + SnapKit Cell {#flow-layout}
UICollectionViewFlowLayout oddiy grid va ro'yxatlar yasash uchun ishlatiladi. SnapKit cell ichidagi elementlarni joylashtirishga javobgar bo'lsa, FlowLayout cell'ning o'lchamini belgilaydi.
Self-sizing (O'z-o'zini o'lchaydigan) Cell'lar
Ba'zida cell'ning o'lchami uning ichidagi content'ga bog'liq bo'lishi kerak. Buni amalga oshirish uchun FlowLayout'ga avtomatik o'lchamni beramiz.
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
// SnapKit yordamida cell ichida to'g'ri (tepadan pastga uzluksiz) constraint'lar yozilgan bo'lsa,
// cell o'zini o'zi to'g'ri o'lchaydi.
Grid yasashda ko'pincha bizga aniq o'lchamlar kerak bo'ladi (masalan, 2 ustunli dizayn). Bunda sizeForItemAt ishlatamiz.
extension BasicCollectionViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// Ekranni 2 ga bo'lamiz, o'rtadagi bo'shliqni ayirib tashlaymiz
let padding: CGFloat = 16
let width = (collectionView.bounds.width - padding * 3) / 2
return CGSize(width: width, height: width * 1.2) // Proportsional balandlik
}
}
3. UICollectionViewCompositionalLayout (Zamonaviy usul) {#compositional}
iOS 13+ da Apple murakkab layoutlarni yasash uchun UICollectionViewCompositionalLayout ni taqdim etdi. Bu yerda SnapKit va Layout'ning vazifalari aniq ajratilgan:
- CompositionalLayout: Cell'larning guruhlanishi, section'lar va scroll yo'nalishini (vertikal/gorizontal) boshqaradi.
- SnapKit: Faqatgina bir dona cell ichidagi UI elementlarning joylashuvini (
contentViewga nisbatan) belgilaydi.
Compositional Layout ierarxiyasi quyidagicha:
Layout
└── Section
└── Group (gorizontal yoki vertikal)
└── Item (sizning Cell'ingiz)
4. 🔥 APP STORE LAYOUT (Asosiy qism) {#app-store}
Endi eng qiziq qismiga keldik. Biz haqiqiy App Store "Today" yoki "Apps" sahifasiga o'xshash layout yaratamiz. Xususiyatlari:
- Asosiy scroll vertikal.
- Ichidagi ba'zi qismlar (section'lar) gorizontal scroll bo'ladi.
- DiffableDataSource ishlatamiz (xavfsiz data binding).
A. Modellar (Item va Section)
import UIKit
import SnapKit
enum AppStoreSection: Int, CaseIterable {
case banners = 0
case featuredApps
case categories
case topCharts
}
enum AppStoreItem: Hashable {
case banner(id: Int, imageUrl: String)
case featuredApp(id: Int, name: String, category: String)
case category(id: Int, name: String)
case topChartApp(id: Int, rank: Int, name: String)
}
B. View Controller va Compositional Layout
Bu yerda biz turli xil section'lar uchun turlicha layout'lar yozamiz.
class AppStoreViewController: UIViewController {
private lazy var collectionView: UICollectionView = {
let layout = createLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .systemGroupedBackground
cv.register(BannerCell.self, forCellWithReuseIdentifier: BannerCell.identifier)
cv.register(FeaturedAppCell.self, forCellWithReuseIdentifier: FeaturedAppCell.identifier)
cv.register(CategoryCell.self, forCellWithReuseIdentifier: CategoryCell.identifier)
cv.register(TopChartCell.self, forCellWithReuseIdentifier: TopChartCell.identifier)
cv.register(SectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: SectionHeaderView.identifier)
return cv
}()
private var dataSource: UICollectionViewDiffableDataSource<AppStoreSection, AppStoreItem>!
override func viewDidLoad() {
super.viewDidLoad()
title = "Apps"
navigationController?.navigationBar.prefersLargeTitles = true
setupSubviews()
setupConstraints()
configureDataSource()
applyMockData()
}
private func setupSubviews() {
view.addSubview(collectionView)
}
private func setupConstraints() {
collectionView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
// MARK: - Compositional Layout
private func createLayout() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout { [weak self] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
guard let sectionType = AppStoreSection(rawValue: sectionIndex) else { return nil }
switch sectionType {
case .banners:
return self?.createBannerSection()
case .featuredApps:
return self?.createFeaturedAppsSection()
case .categories:
return self?.createCategoriesSection()
case .topCharts:
return self?.createTopChartsSection()
}
}
}
// 1. Banners (Tepada katta rasm, paging)
private func createBannerSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
// Group height qat'iy 250 point
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(250))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
// Markazlashgan paging gorizontal scroll
section.orthogonalScrollingBehavior = .groupPagingCentered
section.contentInsets = NSDirectionalEdgeInsets(top: 16, leading: 0, bottom: 24, trailing: 0)
return section
}
// 2. Featured Apps (Gorizontal uzluksiz scroll)
private func createFeaturedAppsSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
// Kengligi ekranning 90%i
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.9), heightDimension: .absolute(80))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
section.contentInsets = NSDirectionalEdgeInsets(top: 16, leading: 16, bottom: 24, trailing: 16)
section.interGroupSpacing = 12
// Header qo'shish
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(44))
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
section.boundarySupplementaryItems = [header]
return section
}
// 3. Categories (Vertikal grid - 2 ta ustun)
private func createCategoriesSection() -> 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, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 16, leading: 12, bottom: 24, trailing: 12)
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(44))
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
section.boundarySupplementaryItems = [header]
return section
}
// 4. Top Charts (Gorizontal paging, har guruhda 3 tadan)
private func createTopChartsSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.33))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 0)
// Balandligi kattaroq, ichida 3 ta item vertikal joylashadi
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.92), heightDimension: .absolute(240))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitem: item, count: 3)
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPaging
section.contentInsets = NSDirectionalEdgeInsets(top: 16, leading: 16, bottom: 24, trailing: 16)
section.interGroupSpacing = 16
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(44))
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
section.boundarySupplementaryItems = [header]
return section
}
}
C. SnapKit bilan yozilgan Cell'lar
Haqiqiy SnapKit sehri shu yerda ro'y beradi!
1. Banner Cell
class BannerCell: UICollectionViewCell {
static let identifier = "BannerCell"
private let imageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
iv.backgroundColor = .systemTeal
iv.layer.cornerRadius = 12
iv.clipsToBounds = true
return iv
}()
private let titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 24, weight: .bold)
label.textColor = .white
label.text = "Featured Title"
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
setupConstraints()
}
required init?(coder: NSCoder) { fatalError() }
private func setupSubviews() {
contentView.addSubview(imageView)
imageView.addSubview(titleLabel) // Rasm ustiga matn
}
private func setupConstraints() {
imageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
titleLabel.snp.makeConstraints { make in
make.leading.bottom.equalToSuperview().inset(16)
make.trailing.equalToSuperview().inset(16)
}
}
}
2. Featured App Cell (Icon + Name + Category + Get Button)
class FeaturedAppCell: UICollectionViewCell {
static let identifier = "FeaturedAppCell"
private let iconImageView: UIImageView = {
let iv = UIImageView()
iv.backgroundColor = .systemOrange
iv.layer.cornerRadius = 16
iv.layer.cornerCurve = .continuous
iv.clipsToBounds = true
return iv
}()
private let textStackView: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 4
stack.alignment = .leading
return stack
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18, weight: .semibold)
return label
}()
private let categoryLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 14, weight: .regular)
label.textColor = .secondaryLabel
return label
}()
private let getButton: UIButton = {
let btn = UIButton(type: .system)
btn.setTitle("GET", for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 15, weight: .bold)
btn.backgroundColor = .systemGray5
btn.layer.cornerRadius = 15
return btn
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
setupConstraints()
}
required init?(coder: NSCoder) { fatalError() }
private func setupSubviews() {
contentView.addSubview(iconImageView)
contentView.addSubview(textStackView)
textStackView.addArrangedSubview(nameLabel)
textStackView.addArrangedSubview(categoryLabel)
contentView.addSubview(getButton)
}
private func setupConstraints() {
iconImageView.snp.makeConstraints { make in
make.leading.equalToSuperview()
make.centerY.equalToSuperview()
make.size.equalTo(64)
}
getButton.snp.makeConstraints { make in
make.trailing.equalToSuperview()
make.centerY.equalToSuperview()
make.width.equalTo(72)
make.height.equalTo(30)
}
textStackView.snp.makeConstraints { make in
make.leading.equalTo(iconImageView.snp.trailing).offset(12)
make.trailing.equalTo(getButton.snp.leading).offset(-12)
make.centerY.equalToSuperview()
}
}
func configure(name: String, category: String) {
nameLabel.text = name
categoryLabel.text = category
}
}
Senior eslatmasi: E'tibor bering, Layout (Compositional Layout) cell'ning balandligini o'rnatadi. SnapKit orqali esa biz ichki elementlarni markazlashtiramiz (
centerY).
3. Section Header (Supplementary View)
class SectionHeaderView: UICollectionReusableView {
static let identifier = "SectionHeaderView"
let titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 22, weight: .bold)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(titleLabel)
titleLabel.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.bottom.equalToSuperview().inset(8)
}
}
required init?(coder: NSCoder) { fatalError() }
}
D. Data Source ulanishi
AppStoreViewController ichiga qaytib, Data Source qismini to'ldiramiz:
extension AppStoreViewController {
private func configureDataSource() {
dataSource = UICollectionViewDiffableDataSource<AppStoreSection, AppStoreItem>(collectionView: collectionView) { (cv, indexPath, item) -> UICollectionViewCell? in
switch item {
case .banner:
let cell = cv.dequeueReusableCell(withReuseIdentifier: BannerCell.identifier, for: indexPath) as! BannerCell
return cell
case .featuredApp(_, let name, let cat):
let cell = cv.dequeueReusableCell(withReuseIdentifier: FeaturedAppCell.identifier, for: indexPath) as! FeaturedAppCell
cell.configure(name: name, category: cat)
return cell
case .category:
let cell = cv.dequeueReusableCell(withReuseIdentifier: CategoryCell.identifier, for: indexPath) as! CategoryCell
return cell
case .topChartApp:
let cell = cv.dequeueReusableCell(withReuseIdentifier: TopChartCell.identifier, for: indexPath) as! TopChartCell
return cell
}
}
dataSource.supplementaryViewProvider = { (cv, kind, indexPath) in
let header = cv.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: SectionHeaderView.identifier, for: indexPath) as! SectionHeaderView
let section = AppStoreSection(rawValue: indexPath.section)
switch section {
case .featuredApps: header.titleLabel.text = "Tavsiya etiladi"
case .categories: header.titleLabel.text = "Kategoriyalar"
case .topCharts: header.titleLabel.text = "Top Chartlar"
default: header.titleLabel.text = ""
}
return header
}
}
private func applyMockData() {
var snapshot = NSDiffableDataSourceSnapshot<AppStoreSection, AppStoreItem>()
snapshot.appendSections(AppStoreSection.allCases)
snapshot.appendItems([
.banner(id: 1, imageUrl: ""),
.banner(id: 2, imageUrl: ""),
.banner(id: 3, imageUrl: "")
], toSection: .banners)
snapshot.appendItems([
.featuredApp(id: 1, name: "SnapKit Pro", category: "Developer Tools"),
.featuredApp(id: 2, name: "Uzbek Dictionary", category: "Reference"),
.featuredApp(id: 3, name: "SwiftUI Master", category: "Education")
], toSection: .featuredApps)
// ... boshqa sectionlar uchun itemlar
dataSource.apply(snapshot, animatingDifferences: true)
}
}
5. Nested Horizontal ScrollView yondashuvi (Eski usul) {#nested}
Compositional Layout paydo bo'lishidan oldin App Store dizayni juda murakkab usulda qilingan:
- Asosiy ekran
UITableViewdan iborat edi. - Gorizontal scroll bo'ladigan har bir qator uchun maxsus
UITableViewCellyasalib, uning ichigaUICollectionViewjoylashtirilgan.
class NestedTableViewCell: UITableViewCell {
// Cell ichidagi Collection View
private lazy var horizontalCollectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
return cv
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(horizontalCollectionView)
horizontalCollectionView.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.height.equalTo(200) // Qat'iy balandlik berish shart edi!
}
}
// ...
}
Muhim: Bu usulni bilish foydali, chunki eski (legacy) proyektlarda bunga ko'p duch kelasiz. Ammo yangi ilovalarda har doim Compositional Layout ishlating!
6. Section Background Decoration {#decoration}
Compositional Layout'ning yana bir zo'r xususiyati — ma'lum bir section'ning orqasiga fon (background view) qo'yish. SnapKit bu background view ichidagi dizaynni qilishda ishlatiladi.
// Layout ichida:
let section = NSCollectionLayoutSection(group: group)
let backgroundItem = NSCollectionLayoutDecorationItem.background(elementKind: "sectionBackground")
section.decorationItems = [backgroundItem]
// View Controller da ro'yxatdan o'tkazish:
layout.register(MyCustomBackgroundView.self, forDecorationViewOfKind: "sectionBackground")
7. Performance va Best Practices {#performance}
- Cell reuse va SnapKit: Agar siz cell ichidagi view'larni holatiga qarab butunlay boshqa joyga ko'chirishingiz kerak bo'lsa (masalan
prepareForReuseda), doim ham yangidan constraint qo'shmang.snp.remakeConstraintso'rniga yashirin (hidden) qilib qo'yish yoki bitta reference'ni yangilash (updateConstraints) performanceni oshiradi. - Prefetching: Rasmlarni yuklash og'ir jarayon bo'lgani uchun,
collectionView.isPrefetchingEnabled = true(default holati shunday) dan foydalanib rasmlarni barvaqt yuklash (UICollectionViewDataSourcePrefetchingorqali) tavsiya etiladi.
Xulosa {#xulosa}
UICollectionView va SnapKit birgalikda iOS'dagi eng kuchli layout duetlaridan birini hosil qiladi. Compositional Layout yordamida "Nima (qanday sectionlar) qayerda turishi kerakligini" belgilasak, SnapKit yordamida "O'sha narsaning ichki ko'rinishi qanday bo'lishi kerakligini" eng sodda tarzda yozamiz.
App Store'ning bugungi sahifasi aynan shu 2 ta texnologiyaning uyg'unlashuvidan kelib chiqqan va bu bilimlar sizni haqiqiy Senior darajasidagi interfeyslar yozishga tayyorlaydi.