# SnapKit 13: Qayta Ishlatiluvchi Komponentlar — BaseView Arxitekturasi, Custom Controls va Design System

> **Holat:** Ushbu darslik 2026-yil oktabr holatiga ko'ra eng zamonaviy iOS dasturlash amaliyotlari (Swift 6, SnapKit 5.7+) va zamonaviy arxitektura tamoyillari asosida tayyorlangan. Reusable (qayta ishlatiluvchi) komponentlar yaratish yirik loyihalarning eng muhim qismidir.

## Mundarija
1. [BaseView Pattern (Eng muhim arxitektura)](#baseview)
2. [Komponent 1: CustomTextField (Floating Label)](#textfield)
3. [Komponent 2: PrimaryButton (Loading holati)](#button)
4. [Komponent 3: BadgeView](#badge)
5. [Komponent 4: RatingView (Yulduzchalar)](#rating)
6. [Komponent 5: AvatarView](#avatar)
7. [Komponent 6: CardView](#card)
8. [Komponent 7: EmptyStateView](#empty)
9. [Design System Tushunchasi](#design-system)
10. [Amaliy Loyiha: Komponentlarni Birlashtirish](#amaliy)
11. [Xulosa](#xulosa)
12. [Manbalar](#manbalar)

---

## 1. BaseView Pattern (Eng muhim arxitektura) {#baseview}

Ilovalarni ishlab chiqishda UI komponentlar yaratish qoidalari aniq bir standartga bo'ysunishi kerak. Agar har bir dasturchi `view` larni o'zicha sozlasa, kod tezda tushunarsiz va parokanda bo'lib ketadi. Buning oldini olish uchun **BaseView Pattern** ishlatiladi.

> **Senior eslatmasi:** Barcha custom view'laringiz aynan bitta poydevordan (BaseView) meros olishi kerak. Bu orqali siz hayot siklini (lifecycle) standartlaysiz: doim birinchi `subviews` qo'shiladi, keyin `constraints` va oxirida `bindings` (harakatlar/reaksiyalar).

```swift
import UIKit
import SnapKit

@MainActor
open class BaseView: UIView {
    
    public override init(frame: CGRect) {
        super.init(frame: frame)
        setupSubviews()
        setupConstraints()
        setupBindings()
    }
    
    public required init?(coder: NSCoder) {
        fatalError("init(coder:) ishlari Interface Builder uchun. Biz faqat kodda ishlaymiz!")
    }
    
    /// View'larni qo'shish uchun (addSubview)
    open func setupSubviews() {}
    
    /// SnapKit constraint'larini o'rnatish uchun
    open func setupConstraints() {}
    
    /// Target-action, gesture recognizers va callback'larni sozlash uchun
    open func setupBindings() {}
}
```

Ushbu arxitekturaning afzalligi shundaki, keyingi barcha komponentlarimiz faqat shu 3 ta metodni `override` qiladi, bu kod o'qiluvchanligini 10 barobar oshiradi.

---

## 2. Komponent 1: CustomTextField (Floating Label) {#textfield}

Zamonaviy dizaynlarda, foydalanuvchi yozishni boshlaganda placeholder tepaga ko'tariluvchi (floating) matn maydonlari juda mashhur. Shuningdek, xato yuz bergan holat (Error state) ham bo'lishi kerak.

```text
Normal Holat:
+-----------------------------------+
|  [Icon]   Ismingizni kiriting     |
+-----------------------------------+

Yozilayotganda (Floating):
  Ismingiz
+-----------------------------------+
|  [Icon]   Azimjon|                |
+-----------------------------------+
```

```swift
import UIKit
import SnapKit

@MainActor
final class CustomTextField: BaseView {
    
    private let iconImageView = UIImageView()
    private let textField = UITextField()
    private let floatingLabel = UILabel()
    private let errorLabel = UILabel()
    private let containerView = UIView()
    
    private var topConstraint: Constraint?
    
    override func setupSubviews() {
        addSubview(containerView)
        addSubview(floatingLabel)
        addSubview(errorLabel)
        
        containerView.addSubview(iconImageView)
        containerView.addSubview(textField)
        
        // Stil sozlamalari
        containerView.layer.borderWidth = 1
        containerView.layer.borderColor = UIColor.systemGray4.cgColor
        containerView.layer.cornerRadius = 8
        
        floatingLabel.font = .systemFont(ofSize: 12)
        floatingLabel.textColor = .systemGray
        floatingLabel.alpha = 0
        
        errorLabel.font = .systemFont(ofSize: 12)
        errorLabel.textColor = .systemRed
        errorLabel.isHidden = true
        
        textField.addTarget(self, action: #selector(textDidChange), for: .editingChanged)
        textField.addTarget(self, action: #selector(editingDidBegin), for: .editingDidBegin)
        textField.addTarget(self, action: #selector(editingDidEnd), for: .editingDidEnd)
    }
    
    override func setupConstraints() {
        containerView.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(16)
            make.leading.trailing.equalToSuperview()
            make.height.equalTo(48)
        }
        
        iconImageView.snp.makeConstraints { make in
            make.leading.equalToSuperview().offset(12)
            make.centerY.equalToSuperview()
            make.size.equalTo(20)
        }
        
        textField.snp.makeConstraints { make in
            make.leading.equalTo(iconImageView.snp.trailing).offset(8)
            make.trailing.equalToSuperview().offset(-12)
            make.centerY.equalToSuperview()
        }
        
        floatingLabel.snp.makeConstraints { make in
            make.leading.equalTo(containerView.snp.leading).offset(4)
            // Boshlang'ich holatda container markazida, float bo'lganda tepaga chiqadi
            self.topConstraint = make.centerY.equalTo(containerView.snp.centerY).constraint
        }
        
        errorLabel.snp.makeConstraints { make in
            make.top.equalTo(containerView.snp.bottom).offset(4)
            make.leading.trailing.bottom.equalToSuperview()
        }
    }
    
    func configure(placeholder: String, icon: UIImage?) {
        textField.placeholder = placeholder
        floatingLabel.text = placeholder
        iconImageView.image = icon
        iconImageView.isHidden = icon == nil
        
        if icon == nil {
            textField.snp.remakeConstraints { make in
                make.leading.equalToSuperview().offset(12)
                make.trailing.equalToSuperview().offset(-12)
                make.centerY.equalToSuperview()
            }
        }
    }
    
    func setError(_ message: String?) {
        if let message = message {
            errorLabel.text = message
            errorLabel.isHidden = false
            containerView.layer.borderColor = UIColor.systemRed.cgColor
        } else {
            errorLabel.isHidden = true
            containerView.layer.borderColor = textField.isEditing ? UIColor.systemBlue.cgColor : UIColor.systemGray4.cgColor
        }
    }
    
    @objc private func editingDidBegin() {
        containerView.layer.borderColor = UIColor.systemBlue.cgColor
        animateFloatingLabel(isFloating: true)
    }
    
    @objc private func editingDidEnd() {
        containerView.layer.borderColor = UIColor.systemGray4.cgColor
        if textField.text?.isEmpty == true {
            animateFloatingLabel(isFloating: false)
        }
    }
    
    @objc private func textDidChange() {
        setError(nil) // Yozganda xatoni tozalash
    }
    
    private func animateFloatingLabel(isFloating: Bool) {
        UIView.animate(withDuration: 0.2) {
            if isFloating {
                self.topConstraint?.update(inset: 32)
                self.floatingLabel.alpha = 1
                self.textField.placeholder = ""
            } else {
                self.topConstraint?.update(inset: 0)
                self.floatingLabel.alpha = 0
                self.textField.placeholder = self.floatingLabel.text
            }
            self.layoutIfNeeded()
        }
    }
}
```

---

## 3. Komponent 2: PrimaryButton (Loading holati) {#button}

Asosiy tugmalar tarmoq so'rovlari vaqtida "Loading" holatiga o'tishi va foydalanuvchiga jarayon ketayotganini ko'rsatishi zarur.

```swift
import UIKit
import SnapKit

@MainActor
final class PrimaryButton: BaseView {
    
    private let titleLabel = UILabel()
    private let iconImageView = UIImageView()
    private let activityIndicator = UIActivityIndicatorView(style: .medium)
    private let stackView = UIStackView()
    
    var tapAction: (() -> Void)?
    
    override func setupSubviews() {
        backgroundColor = .systemBlue
        layer.cornerRadius = 12
        
        addSubview(stackView)
        addSubview(activityIndicator)
        
        stackView.axis = .horizontal
        stackView.spacing = 8
        stackView.alignment = .center
        
        stackView.addArrangedSubview(iconImageView)
        stackView.addArrangedSubview(titleLabel)
        
        titleLabel.font = .boldSystemFont(ofSize: 16)
        titleLabel.textColor = .white
        iconImageView.tintColor = .white
        activityIndicator.color = .white
        activityIndicator.hidesWhenStopped = true
        
        // Touch hodisalari uchun tap gesture o'rniga UIControl kabi ishlashini simulyatsiya qilamiz
        let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
        addGestureRecognizer(tap)
        isUserInteractionEnabled = true
    }
    
    override func setupConstraints() {
        snp.makeConstraints { make in
            make.height.equalTo(50)
        }
        
        stackView.snp.makeConstraints { make in
            make.center.equalToSuperview()
        }
        
        iconImageView.snp.makeConstraints { make in
            make.size.equalTo(20)
        }
        
        activityIndicator.snp.makeConstraints { make in
            make.center.equalToSuperview()
        }
    }
    
    func configure(title: String, icon: UIImage? = nil) {
        titleLabel.text = title
        iconImageView.image = icon
        iconImageView.isHidden = icon == nil
    }
    
    func setLoading(_ isLoading: Bool) {
        isUserInteractionEnabled = !isLoading
        alpha = isLoading ? 0.7 : 1.0
        
        if isLoading {
            stackView.isHidden = true
            activityIndicator.startAnimating()
        } else {
            stackView.isHidden = false
            activityIndicator.stopAnimating()
        }
    }
    
    @objc private func handleTap() {
        // Kichik click animatsiyasi
        UIView.animate(withDuration: 0.1, animations: {
            self.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
        }) { _ in
            UIView.animate(withDuration: 0.1) {
                self.transform = .identity
            }
            self.tapAction?()
        }
    }
}
```

---

## 4. Komponent 3: BadgeView {#badge}

Badge odatda xabarnomalar sonini ko'rsatish uchun ishlatiladi. Hajmi matn uzunligiga moslashuvchan bo'lishi kerak.

```swift
import UIKit
import SnapKit

@MainActor
final class BadgeView: BaseView {
    
    private let textLabel = UILabel()
    
    override func setupSubviews() {
        backgroundColor = .systemRed
        addSubview(textLabel)
        
        textLabel.textColor = .white
        textLabel.font = .systemFont(ofSize: 12, weight: .bold)
        textLabel.textAlignment = .center
    }
    
    override func setupConstraints() {
        textLabel.snp.makeConstraints { make in
            make.leading.trailing.equalToSuperview().inset(6)
            make.top.bottom.equalToSuperview().inset(2)
        }
    }
    
    func setNumber(_ count: Int) {
        if count <= 0 {
            isHidden = true
        } else {
            isHidden = false
            textLabel.text = count > 99 ? "99+" : "\(count)"
        }
        setNeedsLayout()
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        // Aylana yoki capsule shakliga o'tkazish
        let radius = bounds.height / 2
        layer.cornerRadius = radius
    }
}
```

---

## 5. Komponent 4: RatingView (Yulduzchalar) {#rating}

```swift
import UIKit
import SnapKit

@MainActor
final class RatingView: BaseView {
    
    private let stackView = UIStackView()
    private var starButtons: [UIButton] = []
    
    var didChangeRating: ((Int) -> Void)?
    var currentRating: Int = 0
    
    override func setupSubviews() {
        addSubview(stackView)
        stackView.axis = .horizontal
        stackView.spacing = 4
        stackView.distribution = .fillEqually
        
        for i in 0..<5 {
            let btn = UIButton()
            btn.setImage(UIImage(systemName: "star"), for: .normal)
            btn.setImage(UIImage(systemName: "star.fill"), for: .selected)
            btn.tintColor = .systemYellow
            btn.tag = i + 1
            btn.addTarget(self, action: #selector(starTapped(_:)), for: .touchUpInside)
            stackView.addArrangedSubview(btn)
            starButtons.append(btn)
        }
    }
    
    override func setupConstraints() {
        stackView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        
        for btn in starButtons {
            btn.snp.makeConstraints { make in
                make.size.equalTo(30)
            }
        }
    }
    
    @objc private func starTapped(_ sender: UIButton) {
        let rating = sender.tag
        setRating(rating)
        didChangeRating?(rating)
    }
    
    func setRating(_ rating: Int) {
        currentRating = rating
        for (index, button) in starButtons.enumerated() {
            button.isSelected = index < rating
        }
    }
}
```

---

## 6. Komponent 5: AvatarView {#avatar}

Avatar komponenti onlayn indikatori va rasm bo'lmaganda initials (ismining bosh harflari) ni o'z ichiga oladi.

```swift
import UIKit
import SnapKit

@MainActor
final class AvatarView: BaseView {
    
    private let imageView = UIImageView()
    private let initialsLabel = UILabel()
    private let onlineIndicator = UIView()
    
    override func setupSubviews() {
        backgroundColor = .systemGray5
        
        addSubview(initialsLabel)
        addSubview(imageView)
        addSubview(onlineIndicator)
        
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        
        initialsLabel.textColor = .systemGray
        initialsLabel.font = .systemFont(ofSize: 20, weight: .bold)
        initialsLabel.textAlignment = .center
        
        onlineIndicator.backgroundColor = .systemGreen
        onlineIndicator.layer.borderColor = UIColor.white.cgColor
        onlineIndicator.layer.borderWidth = 2
    }
    
    override func setupConstraints() {
        imageView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        
        initialsLabel.snp.makeConstraints { make in
            make.center.equalToSuperview()
        }
        
        onlineIndicator.snp.makeConstraints { make in
            make.size.equalTo(14)
            make.bottom.trailing.equalToSuperview()
        }
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        layer.cornerRadius = bounds.height / 2
        imageView.layer.cornerRadius = bounds.height / 2
        onlineIndicator.layer.cornerRadius = onlineIndicator.bounds.height / 2
    }
    
    func configure(image: UIImage?, initials: String, isOnline: Bool) {
        if let img = image {
            imageView.image = img
            initialsLabel.isHidden = true
        } else {
            imageView.image = nil
            initialsLabel.text = initials
            initialsLabel.isHidden = false
        }
        onlineIndicator.isHidden = !isOnline
    }
}
```

---

## 7. Komponent 6: CardView {#card}

Boshqa ko'rinishlarni o'z ichida saqlovchi konteyner.

```swift
import UIKit
import SnapKit

@MainActor
final class CardView: BaseView {
    
    private let headerLabel = UILabel()
    private let contentView = UIView()
    private let containerStack = UIStackView()
    
    override func setupSubviews() {
        backgroundColor = .systemBackground
        layer.cornerRadius = 16
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowOpacity = 0.1
        layer.shadowOffset = CGSize(width: 0, height: 4)
        layer.shadowRadius = 8
        
        addSubview(containerStack)
        containerStack.axis = .vertical
        containerStack.spacing = 12
        
        headerLabel.font = .boldSystemFont(ofSize: 18)
        
        containerStack.addArrangedSubview(headerLabel)
        containerStack.addArrangedSubview(contentView)
    }
    
    override func setupConstraints() {
        containerStack.snp.makeConstraints { make in
            make.edges.equalToSuperview().inset(16)
        }
    }
    
    func setHeader(title: String) {
        headerLabel.text = title
    }
    
    func setContent(_ view: UIView) {
        contentView.subviews.forEach { $0.removeFromSuperview() }
        contentView.addSubview(view)
        
        view.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }
}
```

---

## 8. Komponent 7: EmptyStateView {#empty}

```swift
import UIKit
import SnapKit

@MainActor
final class EmptyStateView: BaseView {
    
    private let iconImageView = UIImageView()
    private let titleLabel = UILabel()
    private let subtitleLabel = UILabel()
    private let actionButton = PrimaryButton()
    
    override func setupSubviews() {
        addSubview(iconImageView)
        addSubview(titleLabel)
        addSubview(subtitleLabel)
        addSubview(actionButton)
        
        iconImageView.tintColor = .systemGray3
        iconImageView.contentMode = .scaleAspectFit
        
        titleLabel.font = .systemFont(ofSize: 20, weight: .bold)
        titleLabel.textAlignment = .center
        
        subtitleLabel.font = .systemFont(ofSize: 14)
        subtitleLabel.textColor = .systemGray
        subtitleLabel.textAlignment = .center
        subtitleLabel.numberOfLines = 0
    }
    
    override func setupConstraints() {
        iconImageView.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(24)
            make.centerX.equalToSuperview()
            make.size.equalTo(80)
        }
        
        titleLabel.snp.makeConstraints { make in
            make.top.equalTo(iconImageView.snp.bottom).offset(16)
            make.leading.trailing.equalToSuperview().inset(24)
        }
        
        subtitleLabel.snp.makeConstraints { make in
            make.top.equalTo(titleLabel.snp.bottom).offset(8)
            make.leading.trailing.equalToSuperview().inset(24)
        }
        
        actionButton.snp.makeConstraints { make in
            make.top.equalTo(subtitleLabel.snp.bottom).offset(24)
            make.leading.trailing.equalToSuperview().inset(40)
            make.bottom.equalToSuperview().offset(-24)
        }
    }
    
    func configure(icon: UIImage?, title: String, subtitle: String, buttonTitle: String, action: @escaping () -> Void) {
        iconImageView.image = icon
        titleLabel.text = title
        subtitleLabel.text = subtitle
        actionButton.configure(title: buttonTitle)
        actionButton.tapAction = action
    }
}
```

---

## 9. Design System Tushunchasi {#design-system}

Katta loyihalarda ranglar, o'lchamlar va shriftlarni to'g'ridan-to'g'ri `16`, `24`, `.systemBlue` deb yozish noto'g'ri. Biz **Design Tokens** (dizayn belgilar) dan foydalanishimiz kerak.

```swift
import UIKit

enum Spacing {
    static let xs: CGFloat = 4
    static let sm: CGFloat = 8
    static let md: CGFloat = 16
    static let lg: CGFloat = 24
    static let xl: CGFloat = 32
}

enum AppColors {
    static let primary = UIColor.systemBlue
    static let background = UIColor.systemGroupedBackground
    static let textMain = UIColor.label
    static let textSecondary = UIColor.secondaryLabel
}

// SnapKit'da ishlatish:
// make.top.equalToSuperview().offset(Spacing.md)
// view.backgroundColor = AppColors.background
```

Bu usul orqali barcha komponentlar bir xil "til" da gaplashadi. Bitta joydan dizaynni o'zgartirish butun ilovaga ta'sir qiladi.

---

## 10. Amaliy Loyiha: Komponentlarni Birlashtirish {#amaliy}

Keling, barcha yaratgan komponentlarimizni bitta Ekranga (Profilni tahrirlash) jamlaymiz.

```swift
import UIKit
import SnapKit

@MainActor
final class EditProfileViewController: UIViewController {
    
    private let scrollView = UIScrollView()
    private let contentView = UIView()
    
    private let avatarView = AvatarView()
    private let nameField = CustomTextField()
    private let emailField = CustomTextField()
    private let ratingView = RatingView()
    private let cardView = CardView()
    private let saveButton = PrimaryButton()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = AppColors.background
        title = "Profil"
        
        setupViews()
        setupConstraints()
        configureComponents()
    }
    
    private func setupViews() {
        view.addSubview(scrollView)
        scrollView.addSubview(contentView)
        
        contentView.addSubview(avatarView)
        contentView.addSubview(nameField)
        contentView.addSubview(emailField)
        contentView.addSubview(cardView)
        contentView.addSubview(saveButton)
        
        // RatingView ni Card ichiga joylashtiramiz
        let ratingContainer = UIView()
        ratingContainer.addSubview(ratingView)
        ratingView.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.top.bottom.equalToSuperview().inset(Spacing.md)
        }
        cardView.setContent(ratingContainer)
    }
    
    private func setupConstraints() {
        scrollView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        
        contentView.snp.makeConstraints { make in
            make.edges.width.equalToSuperview()
        }
        
        avatarView.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(Spacing.xl)
            make.centerX.equalToSuperview()
            make.size.equalTo(100)
        }
        
        nameField.snp.makeConstraints { make in
            make.top.equalTo(avatarView.snp.bottom).offset(Spacing.xl)
            make.leading.trailing.equalToSuperview().inset(Spacing.md)
        }
        
        emailField.snp.makeConstraints { make in
            make.top.equalTo(nameField.snp.bottom).offset(Spacing.md)
            make.leading.trailing.equalToSuperview().inset(Spacing.md)
        }
        
        cardView.snp.makeConstraints { make in
            make.top.equalTo(emailField.snp.bottom).offset(Spacing.xl)
            make.leading.trailing.equalToSuperview().inset(Spacing.md)
        }
        
        saveButton.snp.makeConstraints { make in
            make.top.equalTo(cardView.snp.bottom).offset(Spacing.xl)
            make.leading.trailing.equalToSuperview().inset(Spacing.md)
            make.bottom.equalToSuperview().offset(-Spacing.xl) // ScrollView chegarasi
        }
    }
    
    private func configureComponents() {
        avatarView.configure(image: nil, initials: "AZ", isOnline: true)
        
        nameField.configure(placeholder: "Ismingiz", icon: UIImage(systemName: "person"))
        emailField.configure(placeholder: "Email manzilingiz", icon: UIImage(systemName: "envelope"))
        
        cardView.setHeader(title: "Foydalanuvchi darajasi")
        ratingView.setRating(4)
        
        saveButton.configure(title: "Saqlash")
        saveButton.tapAction = { [weak self] in
            self?.saveButton.setLoading(true)
            // Fake network request
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                self?.saveButton.setLoading(false)
            }
        }
    }
}
```

---

## Xulosa {#xulosa}

Bu darsda eng kuchli va haqiqiy kompaniyalarda ishlatiluvchi **BaseView arxitekturasi** va qayta ishlatiluvchi (reusable) komponentlarni ko'rib chiqdik. SnapKit bilan UI yaratishda **Custom Components** qilish quyidagi afzalliklarni beradi:
1. **Kod takrorlanmaydi** (DRY - Don't Repeat Yourself).
2. UI qismlarini **mustaqil test qilish** imkoniyati.
3. Yangi ekranlar yaratish xuddi **Lego** o'ynashdek oson bo'lib qoladi.

---

## Manbalar {#manbalar}
- [SnapKit Rasmiy Hujjatlari](https://github.com/SnapKit/SnapKit)
- [Apple UIKit Documentation](https://developer.apple.com/documentation/uikit)
- [Swift 6 va MainActor qoidalari](https://developer.apple.com/documentation/swift/mainactor)
