Azimjon Abdurasulov

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 UICollectionViewCompositionalLayout yordamida aynan shunday zamonaviy, murakkab va moslashuvchan dizaynlarni qanday yaratishni o'rganamiz.

Mundarija

  1. UICollectionView + SnapKit asoslari
  2. UICollectionViewFlowLayout + SnapKit Cell
  3. UICollectionViewCompositionalLayout (Zamonaviy usul)
  4. 🔥 APP STORE LAYOUT (Asosiy qism)
  5. Nested Horizontal ScrollView yondashuvi (Eski usul)
  6. Section Background Decoration
  7. Performance va Best Practices
  8. Xulosa
  9. 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:

  1. CompositionalLayout: Cell'larning guruhlanishi, section'lar va scroll yo'nalishini (vertikal/gorizontal) boshqaradi.
  2. SnapKit: Faqatgina bir dona cell ichidagi UI elementlarning joylashuvini (contentView ga 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 UITableView dan iborat edi.
  • Gorizontal scroll bo'ladigan har bir qator uchun maxsus UITableViewCell yasalib, uning ichiga UICollectionView joylashtirilgan.
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}

  1. Cell reuse va SnapKit: Agar siz cell ichidagi view'larni holatiga qarab butunlay boshqa joyga ko'chirishingiz kerak bo'lsa (masalan prepareForReuse da), doim ham yangidan constraint qo'shmang. snp.remakeConstraints o'rniga yashirin (hidden) qilib qo'yish yoki bitta reference'ni yangilash (updateConstraints) performanceni oshiradi.
  2. Prefetching: Rasmlarni yuklash og'ir jarayon bo'lgani uchun, collectionView.isPrefetchingEnabled = true (default holati shunday) dan foydalanib rasmlarni barvaqt yuklash (UICollectionViewDataSourcePrefetching orqali) 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.

Manbalar {#manbalar}

  1. Apple Documentation: UICollectionViewCompositionalLayout
  2. SnapKit Documentation
  3. Modern Collection Views in iOS (WWDC)
  4. Diffable Data Sources

Markdown manba

iOS Lessons