Azimjon Abdurasulov

SnapKit 14: Katta Amaliy Loyiha va Debugging — E-Commerce Ekrani, .labeled(), Xotira va Best Practices

🧩 SnapKit — Zero to Hero

SnapKit 14: Katta Amaliy Loyiha va Debugging — E-Commerce Ekrani, .labeled(), Xotira va Best Practices

Holat: 2026-yil oktabr holatiga ko'ra dolzarb va tekshirilgan. Barcha kodlar Swift 6, iOS 15+ va SnapKit 5.7+ da ishlaydi.

Ushbu so'nggi va eng katta Capstone darsimizda SnapKit-dagi debugging usullari, xotira (memory) xavflari, best practice'lar va katta amaliy loyiha sifatida haqiqiy E-Commerce (Elektron tijorat) ekranini noldan yaratamiz.

Mundarija

  1. Debugging SnapKit Constraintlar
  2. Xotira va Retain Cycle xavflari
  3. SnapKit Best Practices (Professional qoidalar)
  4. KATTA AMALIY LOYIHA: E-Commerce Mahsulot Sahifasi
  5. Unit Test Yozish (Bonus)
  6. Kurs Yakuniy Xulosa
  7. Xulosa
  8. Manbalar

1. Debugging SnapKit Constraintlar {#debugging}

Dastur tuzishda constraint'lar doim ham birinchi urinishda to'g'ri bo'lmasligi mumkin. Auto Layout qizil rangli uzun va tushunarsiz xatolarni (console warnings) chiqaradi. Buni SnapKit orqali oson hal qilish yo'llari mavjud.

.labeled() orqali nomlash

Constraint'ga nom berish Auto Layout xatolarida aynan qaysi constraint buzilayotganini topishga yordam beradi:

import UIKit
import SnapKit

let profileImageView = UIImageView()
view.addSubview(profileImageView)

profileImageView.snp.makeConstraints { make in
    // Nomi Auto Layout loglarida chiqadi
    make.top.equalToSuperview().offset(20).labeled("profileAvatarTop")
    make.width.height.equalTo(100).labeled("profileAvatarSize")
    make.centerX.equalToSuperview().labeled("profileAvatarCenterX")
}

Agar ushbu constraint buzilsa (conflict), konsolda NSLayoutConstraint xatosi ichida profileAvatarTop nomi yaqqol ko'rinadi.

LLDB da ko'rish

Dastur to'xtatilganda (breakpoint orqali) quyidagi komandani konsolga yozib view'ning constraintlarini ko'rishingiz mumkin:

po profileImageView.snp.layoutConstraints

Ambiguous Layout (Noaniq joylashuv)

Ba'zan view qayerda turishi kerakligini yoki qanday o'lchamda bo'lishini yetarlicha constraint berilmaganligi sababli Auto Layout aniqlay olmaydi. Buni aniqlash uchun:

if profileImageView.hasAmbiguousLayout {
    print("profileImageView ning layouti noaniq!")
}

// Yoki vizual tekshirish uchun (debug vaqtida view'ni har xil holatga o'zgartirib ko'radi)
profileImageView.exerciseAmbiguityInLayout()

Bundan tashqari Xcode'ning Debug View Hierarchy tugmasi orqali (View Debugger) dastur ishlayotganida UI'ni 3D formatda va barcha xatolarni o'ng paneldan (Issue Navigator) ko'rishingiz mumkin.


2. Xotira va Retain Cycle xavflari {#xotira}

SnapKit ishlatayotganda makeConstraints, updateConstraints, remakeConstraints bloklarida retain cycle bo'ladimi?

Senior eslatmasi: SnapKit closure'larida [weak self] KERAK EMAS!

SnapKit bu closure'larni saqlab qolmaydi (capture qilib turmaydi). U darhol bajariladi va tugaydi. Ammo agar siz closure ichida view'ning property'lariga murojaat qilsangiz, retain cycle xavfi umuman yo'q.

class MyView: UIView {
    let someView = UIView()
    var offsetValue: CGFloat = 20

    func setup() {
        addSubview(someView)
        someView.snp.makeConstraints { make in
            // [weak self] yozish shart emas
            make.top.equalToSuperview().offset(self.offsetValue)
        }
    }
}

Biroq, agar closure ichida nimanidir asinxron ravishda bajarsangiz (bu SnapKit'da bo'lmaydi), shunda weak ishlatiladi. Constraint obyekti view'ning o'ziga tegishli bo'lganligi uchun unga ishora (reference) saqlashda ham weak ishlatilmaydi:

// To'g'ri
var topConstraint: Constraint? 

Xotira sizib chiqishi (Memory Leak) bor yo'qligini Xcode'dagi Memory Graph Debugger orqali tekshirishni unutmang.


3. SnapKit Best Practices (Professional qoidalar) {#best-practices}

14 dars davomida o'rganganlarimiz asosida professional standartlar ro'yxati:

Qoida turiNima qilish kerak / Nima qilmaslik kerak
✅ DOIMUITableViewCell yoki UICollectionViewCell larda constraintlarni contentView ga bering. self ga emas.
✅ DOIMaddSubview ni doim makeConstraints dan OLDIN chaqiring. Aks holda crash bo'ladi.
✅ DOIMDinamik (o'zgarib turadigan) constraint'larga .priority(999) yoki undan pastroq bering. Agar conflict bo'lsa, .required (1000) dasturni ishdan chiqarmaydi (xato yozadi xolos).
✅ DOIMFaqat doimiyni (constant / offset) o'zgartirish uchun updateConstraints ishlating.
✅ DOIMLayout butunlay o'zgarganda (masalan, portrait'dan landscape'ga) remakeConstraints ishlating. Yoki shunchaki Constraint turidagi o'zgaruvchini yangilang.
✅ DOIMMuhim UI elementlarga va murakkab joylarga .labeled("name") qo'shing.
❌ QILMANGBitta view'ga ikki marta alohida makeConstraints chaqirmang. Ikkinchisini update yoki remake qiling.
❌ QILMANGtranslatesAutoresizingMaskIntoConstraints ni qo'lda yozib o'tirmang. SnapKit make... chaqirilganda buni o'zi false qiladi.
❌ QILMANGAuto Layout ishlagan view'larga (SnapKit qilingan) qo'lda frame bermang. Bu ziddiyat yaratadi.

4. KATTA AMALIY LOYIHA: E-Commerce Mahsulot Sahifasi {#loyiha}

Ushbu amaliyotda biz to'liq ishlaydigan E-Commerce Product Page yaratamiz. U Sticky bottom bar, scroll view ichida turli xil ma'lumotlar va custom view'lardan iborat.

A. Asosiy Controller va ScrollView sozlamalari

import UIKit
import SnapKit

@MainActor
final class ProductViewController: UIViewController {
    
    // UI Komponentlari
    private let scrollView = UIScrollView()
    private let contentView = UIView()
    
    // Custom Viewlar
    private let carouselView = ProductImageCarousel()
    private let infoView = ProductInfoView()
    private let variantView = VariantSelectorView()
    private let descriptionView = DescriptionSectionView()
    private let stickyBottomBar = StickyBottomBar()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    private func setupUI() {
        view.backgroundColor = .white
        
        // Navigation bar sozlamalari
        title = "Mahsulot Tafsilotlari"
        let cartButton = UIBarButtonItem(image: UIImage(systemName: "cart"), style: .plain, target: nil, action: nil)
        navigationItem.rightBarButtonItem = cartButton
        
        // Asosiy viewlarni qo'shish
        view.addSubview(scrollView)
        scrollView.addSubview(contentView)
        view.addSubview(stickyBottomBar)
        
        // Kontentni scrollView ichiga qo'shish
        contentView.addSubview(carouselView)
        contentView.addSubview(infoView)
        contentView.addSubview(variantView)
        contentView.addSubview(descriptionView)
        
        setupConstraints()
    }
    
    private func setupConstraints() {
        // Sticky Bottom Bar - doim pastda
        stickyBottomBar.snp.makeConstraints { make in
            make.leading.trailing.bottom.equalToSuperview().labeled("bottomBarEdges")
            // Balandligi o'z-o'zidan ichidagi elementlardan aniqlanadi
        }
        
        // ScrollView - pastki qismi stickyBar'ning tepasigacha
        scrollView.snp.makeConstraints { make in
            make.top.leading.trailing.equalToSuperview().labeled("scrollViewTopEdges")
            make.bottom.equalTo(stickyBottomBar.snp.top).labeled("scrollViewBottom")
        }
        
        // ContentView
        contentView.snp.makeConstraints { make in
            make.edges.equalToSuperview().labeled("contentEdges")
            make.width.equalTo(scrollView.snp.width).labeled("contentWidth")
        }
        
        // 1. Carousel
        carouselView.snp.makeConstraints { make in
            make.top.leading.trailing.equalToSuperview().labeled("carouselEdges")
            // 3:4 aspect ratio (balandlik = kenglik * 1.33)
            make.height.equalTo(carouselView.snp.width).multipliedBy(1.33).labeled("carouselHeight")
        }
        
        // 2. Product Info (Narx, sarlavha)
        infoView.snp.makeConstraints { make in
            make.top.equalTo(carouselView.snp.bottom).offset(16).labeled("infoTop")
            make.leading.trailing.equalToSuperview().inset(16)
        }
        
        // 3. Variant Selector
        variantView.snp.makeConstraints { make in
            make.top.equalTo(infoView.snp.bottom).offset(24).labeled("variantTop")
            make.leading.trailing.equalToSuperview().inset(16)
        }
        
        // 4. Description
        descriptionView.snp.makeConstraints { make in
            make.top.equalTo(variantView.snp.bottom).offset(24).labeled("descriptionTop")
            make.leading.trailing.equalToSuperview().inset(16)
            make.bottom.equalToSuperview().offset(-24).labeled("descriptionBottom") // Eng muhimi: Contentview height ni belgilaydi
        }
    }
}

B. Custom View komponentlar (Qisqartirilgan, ammo to'liq ishlaydigan)

ProductInfoView.swift

import UIKit
import SnapKit

class ProductInfoView: UIView {
    private let titleLabel: UILabel = {
        let label = UILabel()
        label.text = "Premium Yozgi Krossovka - Qora/Oq rangda, 2026 Model"
        label.font = .systemFont(ofSize: 20, weight: .semibold)
        label.numberOfLines = 0
        return label
    }()
    
    private let priceLabel: UILabel = {
        let label = UILabel()
        label.text = "$129.99"
        label.font = .systemFont(ofSize: 24, weight: .bold)
        label.textColor = .systemRed
        return label
    }()
    
    private let originalPriceLabel: UILabel = {
        let label = UILabel()
        let attributeString = NSMutableAttributedString(string: "$199.99")
        attributeString.addAttribute(NSAttributedString.Key.strikethroughStyle, value: 1, range: NSRange(location: 0, length: attributeString.length))
        label.attributedText = attributeString
        label.textColor = .gray
        return label
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }
    required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
    
    private func setup() {
        addSubview(titleLabel)
        addSubview(priceLabel)
        addSubview(originalPriceLabel)
        
        priceLabel.snp.makeConstraints { make in
            make.top.leading.equalToSuperview()
        }
        
        originalPriceLabel.snp.makeConstraints { make in
            make.leading.equalTo(priceLabel.snp.trailing).offset(8)
            make.bottom.equalTo(priceLabel.snp.bottom)
        }
        
        titleLabel.snp.makeConstraints { make in
            make.top.equalTo(priceLabel.snp.bottom).offset(12)
            make.leading.trailing.equalToSuperview()
            make.bottom.equalToSuperview() // View balandligini yopish
        }
    }
}

StickyBottomBar.swift

import UIKit
import SnapKit

class StickyBottomBar: UIView {
    private let separatorView = UIView()
    private let stepper = UIStepper()
    private let addToCartButton = UIButton(type: .system)
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }
    required init?(coder: NSCoder) { fatalError() }
    
    private func setup() {
        backgroundColor = .white
        separatorView.backgroundColor = .systemGray5
        
        addToCartButton.setTitle("Savatga qo'shish", for: .normal)
        addToCartButton.backgroundColor = .systemBlue
        addToCartButton.setTitleColor(.white, for: .normal)
        addToCartButton.layer.cornerRadius = 8
        addToCartButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold)
        
        addSubview(separatorView)
        addSubview(stepper)
        addSubview(addToCartButton)
        
        separatorView.snp.makeConstraints { make in
            make.top.leading.trailing.equalToSuperview()
            make.height.equalTo(1)
        }
        
        stepper.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(16)
            make.leading.equalToSuperview().offset(16)
            // Safe area hisobga olinadi, masalan iPhone 14 Pro
            make.bottom.equalTo(safeAreaLayoutGuide.snp.bottom).offset(-16)
        }
        
        addToCartButton.snp.makeConstraints { make in
            make.centerY.equalTo(stepper.snp.centerY)
            make.leading.equalTo(stepper.snp.trailing).offset(16)
            make.trailing.equalToSuperview().offset(-16)
            make.height.equalTo(50)
        }
    }
}

Muhim: Barcha custom view'larni yasashda tepadan pastga constraintlarni ulang va eng so'nggi elementning bottom'ini o'z superview'sining bottom'iga ulang. Shunda custom view ichidagi kontentga qarab avtomatik o'z balandligini aniqlaydi (Self-sizing).


5. Unit Test Yozish (Bonus) {#unit-test}

Constraint'lar kutilgandek ishlayotganligini kod orqali tekshirish (Unit testing) iOS dasturlashda juda muhim. Tasavvur qiling, tugmaning kengligi 100 ga teng ekanini test qilmoqchisiz:

import XCTest
import SnapKit
@testable import MyApp

final class MyViewTests: XCTestCase {
    
    @MainActor
    func testButtonConstraints() {
        // 1. Arrange
        let testView = UIView(frame: CGRect(x: 0, y: 0, width: 375, height: 812)) // iPhone screen
        let button = UIButton()
        
        testView.addSubview(button)
        button.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.width.equalTo(150)
            make.height.equalTo(50)
        }
        
        // 2. Act
        // Auto layout'ni majburiy ishga tushirish (frame'larni hisoblash uchun)
        testView.setNeedsLayout()
        testView.layoutIfNeeded()
        
        // 3. Assert
        XCTAssertEqual(button.frame.width, 150)
        XCTAssertEqual(button.frame.height, 50)
        XCTAssertEqual(button.center, CGPoint(x: 375/2, y: 812/2))
    }
}

Bunga qo'shimcha ravishda Snapshot Testing (masalan, swift-snapshot-testing kutubxonasi) orqali butun ekranning rasmini olib tekshirish layoutlar uchun ancha samarali.


6. Kurs Yakuniy Xulosa {#yakun}

Siz ushbu kurs orqali 14 ta dars davomida SnapKit bilan ishlash bo'yicha to'liq professionallikka erishdingiz!

Biz nimalarni o'rgandik?

  1. SnapKit asoslari: snp.makeConstraints, anchorlar, offsetlar, insetlar.
  2. Kengaytirilgan layout: Prioritetlar, Update va Remake constraints, StackView, safeAreaLayoutGuide va ScrollView.
  3. Animation va dinamiklik: Constraint turidagi o'zgaruvchilarni saqlash, .offset() ni animatsiya ichida layoutIfNeeded() bilan o'zgartirish.
  4. TableView/CollectionView: Avtomatik balandlik (self-sizing), contentView.
  5. Debugging va Best Practices: Hozirgi darsimizdagi to'liq E-Commerce ekrani va arxitektura qoidalari.

Keyingi qadamlar:

  • Apple endi SwiftUI'ni faol targ'ib qilmoqda. Lekin o'n minglab katta loyihalar va murakkab dizaynlar hali ham UIKit va SnapKit'da yozilgan.
  • Siz endi barcha murakkab ekranlarni erkin yoza olasiz. UIKit va SwiftUI'ni birgalikda (interoperability) ishlata bilish qobiliyatini rivojlantiring (masalan UIHostingController orqali).
  • Hali ham Native NSLayoutConstraint (Anchorlar) asoslari va ishlash mexanizmini to'liq tushunib oling, chunki SnapKit ularning ustidagi shunchaki chiroyli "qopqoq" (wrapper) xolos.

Xulosa {#xulosa}

KURS YAKUNIY XULOSASI: SnapKit – iOS dunyosidagi eng mashhur, mustahkam va barqaror Auto Layout kutubxonasidir. Katta E-commerce ilovasini yaratish orqali ko'rdikki, har qanday dizaynni modullarga bo'lib (custom views), ularni scroll view ichiga joylash SnapKit yordamida juda kam kod bilan hal etiladi. .labeled() funksiyasi yordamida UI xatolarini zudlik bilan aniqlash, memory retain cycle'larsiz bloklarni erkin ishlatish – developerlarga xotirjamlik va tezlik beradi. Siz ushbu kurs davomida SnapKit noldan boshlab professional darajagacha o'zlashtirdingiz, endi har qanday kompaniyaning UIKit loyihasida erkin va ishonch bilan ishlay olasiz.

Manbalar {#manbalar}

Markdown manba

iOS Lessons