SnapKit 13: Qayta Ishlatiluvchi Komponentlar — BaseView Arxitekturasi, Custom Controls va Design System
🧩 SnapKit — Zero to Hero
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
- BaseView Pattern (Eng muhim arxitektura)
- Komponent 1: CustomTextField (Floating Label)
- Komponent 2: PrimaryButton (Loading holati)
- Komponent 3: BadgeView
- Komponent 4: RatingView (Yulduzchalar)
- Komponent 5: AvatarView
- Komponent 6: CardView
- Komponent 7: EmptyStateView
- Design System Tushunchasi
- Amaliy Loyiha: Komponentlarni Birlashtirish
- Xulosa
- 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
subviewsqo'shiladi, keyinconstraintsva oxiridabindings(harakatlar/reaksiyalar).
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.
Normal Holat:
+-----------------------------------+
| [Icon] Ismingizni kiriting |
+-----------------------------------+
Yozilayotganda (Floating):
Ismingiz
+-----------------------------------+
| [Icon] Azimjon| |
+-----------------------------------+
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.
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.
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}
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.
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.
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}
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.
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.
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:
- Kod takrorlanmaydi (DRY - Don't Repeat Yourself).
- UI qismlarini mustaqil test qilish imkoniyati.
- Yangi ekranlar yaratish xuddi Lego o'ynashdek oson bo'lib qoladi.