Azimjon Abdurasulov

SnapKit 17: Expert-Level Custom Views — SwiftUI'da qiyin, SnapKit'da oson bo'lgan murakkab narsalar

🧩 SnapKit — Zero to Hero

SnapKit 16: Expert-Level Custom Views — SwiftUI'da qiyin, SnapKit'da oson bo'lgan murakkab narsalar

Holat: 2026-yil oktabr holatiga ko'ra, SwiftUI qanchalik rivojlanmasin, ba'zi murakkab interaktiv va maxsus ko'rinishlarni yaratishda UIKit va SnapKit hamon oltin standart hisoblanadi. Ushbu darsda biz SwiftUI'da yozish qiyin yoki samaradorligi past bo'lgan, biroq SnapKit orqali juda tabiiy va tez ishlaydigan professional darajadagi Custom View'larni ko'rib chiqamiz.

Mundarija

  1. Custom UIControl subclass — Interactive Slider
  2. Pinch-to-Zoom Image Viewer
  3. Stretchy Header (Parallax)
  4. Custom TabBar (Bottom Navigation)
  5. Draggable Bottom Sheet (Apple Maps uslubi)
  6. Animated Segmented Control
  7. Circular Progress Ring (Apple Watch uslubi)
  8. Interactive Card Stack (Tinder uslubi)
  9. Custom Alert/Dialog
  10. Performance Comparison
  11. Expert Tips
  12. Xulosa
  13. Manbalar

1. Custom UIControl subclass — Interactive Slider {#interactive-slider}

AppStore'dagi narx filtri kabi ikkita thumb (min va max) ga ega bo'lgan range slider yaratamiz.

SwiftUI'da buni custom Shape va DragGesture orqali yig'ish noqulay va state'larni boshqarish performance muammolarini keltirib chiqarishi mumkin. UIKit'da esa UIControl subclass'i sifatida oson yaratiladi.

[   (o)===========----(o)     ]
  Min                Max
import UIKit
import SnapKit

@MainActor
class RangeSlider: UIControl {
    var minimumValue: CGFloat = 0.0 { didSet { updateLayerFrames() } }
    var maximumValue: CGFloat = 1.0 { didSet { updateLayerFrames() } }
    var lowerValue: CGFloat = 0.2 { didSet { updateLayerFrames() } }
    var upperValue: CGFloat = 0.8 { didSet { updateLayerFrames() } }
    
    private let trackLayer = CALayer()
    private let highlightLayer = CALayer()
    private let lowerThumb = UIView()
    private let upperThumb = UIView()
    
    private var previousLocation = CGPoint()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }
    
    required init?(coder: NSCoder) { fatalError() }
    
    private func setupViews() {
        trackLayer.backgroundColor = UIColor.systemGray5.cgColor
        layer.addSublayer(trackLayer)
        
        highlightLayer.backgroundColor = UIColor.systemBlue.cgColor
        layer.addSublayer(highlightLayer)
        
        [lowerThumb, upperThumb].forEach { thumb in
            thumb.backgroundColor = .white
            thumb.layer.cornerRadius = 15
            thumb.layer.shadowColor = UIColor.black.cgColor
            thumb.layer.shadowOffset = CGSize(width: 0, height: 2)
            thumb.layer.shadowOpacity = 0.3
            addSubview(thumb)
            
            let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
            thumb.addGestureRecognizer(pan)
            thumb.isUserInteractionEnabled = true
        }
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        updateLayerFrames()
    }
    
    private func updateLayerFrames() {
        let trackHeight: CGFloat = 4
        let width = bounds.width
        
        trackLayer.frame = CGRect(x: 0, y: (bounds.height - trackHeight) / 2, width: width, height: trackHeight)
        
        let lowerX = (lowerValue - minimumValue) / (maximumValue - minimumValue) * width
        let upperX = (upperValue - minimumValue) / (maximumValue - minimumValue) * width
        
        highlightLayer.frame = CGRect(x: lowerX, y: trackLayer.frame.minY, width: upperX - lowerX, height: trackHeight)
        
        lowerThumb.frame = CGRect(x: lowerX - 15, y: (bounds.height - 30) / 2, width: 30, height: 30)
        upperThumb.frame = CGRect(x: upperX - 15, y: (bounds.height - 30) / 2, width: 30, height: 30)
    }
    
    @objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
        guard let thumb = gesture.view else { return }
        let location = gesture.location(in: self)
        
        if gesture.state == .began {
            previousLocation = location
        } else if gesture.state == .changed {
            let delta = location.x - previousLocation.x
            let deltaValue = delta / bounds.width * (maximumValue - minimumValue)
            
            if thumb == lowerThumb {
                lowerValue = max(minimumValue, min(lowerValue + deltaValue, upperValue))
            } else {
                upperValue = max(lowerValue, min(upperValue + deltaValue, maximumValue))
            }
            
            previousLocation = location
            sendActions(for: .valueChanged)
        }
    }
}

Senior eslatmasi: Biz bu yerda constraint'larni har bir harakatda yangilash o'rniga frame bazasidagi layoutni ishlatdik (layoutSubviews). Slider kabi uzluksiz chiziladigan, juda tez animatsiya talab qiluvchi komponentlarda CALayer va frame ishlari SnapKit constraintlariga qaraganda samaraliroq bo'lishi mumkin. Lekin butun RangeSlider'ning o'zini ekranga SnapKit bilan joylashtiramiz.


2. Pinch-to-Zoom Image Viewer {#pinch-zoom}

Rasm ustiga ikki marta bosganda yoki chimchilaganda zoom bo'ladigan viewer. SwiftUI MagnificationGesture bor, lekin u to'laqonli pan-zoom tajribasini taqdim etmaydi (chegaralardan chiqib ketish, inertial pan kabi muammolar bor). UIKit'da esa UIScrollView buni tekinga beradi.

import UIKit
import SnapKit

@MainActor
class ZoomableImageViewer: UIView, UIScrollViewDelegate {
    private let scrollView = UIScrollView()
    private let imageView = UIImageView()
    
    init(image: UIImage?) {
        super.init(frame: .zero)
        imageView.image = image
        setupSubviews()
        setupConstraints()
    }
    
    required init?(coder: NSCoder) { fatalError() }
    
    private func setupSubviews() {
        scrollView.delegate = self
        scrollView.minimumZoomScale = 1.0
        scrollView.maximumZoomScale = 4.0
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.showsVerticalScrollIndicator = false
        addSubview(scrollView)
        
        imageView.contentMode = .scaleAspectFit
        imageView.isUserInteractionEnabled = true
        scrollView.addSubview(imageView)
        
        let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:)))
        doubleTap.numberOfTapsRequired = 2
        imageView.addGestureRecognizer(doubleTap)
    }
    
    private func setupConstraints() {
        scrollView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        
        imageView.snp.makeConstraints { make in
            make.edges.equalTo(scrollView.contentLayoutGuide)
            make.size.equalTo(scrollView.frameLayoutGuide) // Center it by default
        }
    }
    
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return imageView
    }
    
    @objc private func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
        if scrollView.zoomScale > scrollView.minimumZoomScale {
            scrollView.setZoomScale(scrollView.minimumZoomScale, animated: true)
        } else {
            let point = gesture.location(in: imageView)
            let zoomRect = CGRect(x: point.x - 50, y: point.y - 50, width: 100, height: 100)
            scrollView.zoom(to: zoomRect, animated: true)
        }
    }
}

3. Stretchy Header (Parallax) {#stretchy-header}

Pastga tortganda rasm cho'ziladigan parallax effekt. SwiftUI'da ScrollView va GeometryReader yordamida qilinadi, biroq layout recalculation'lari tufayli titrashlar (jitter) yuzaga kelishi mumkin. SnapKit va UIScrollViewDelegate bilan esa juda silliq chiqadi.

import UIKit
import SnapKit

@MainActor
class StretchyHeaderViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    private let tableView = UITableView()
    private let headerImageView = UIImageView()
    private var headerTopConstraint: Constraint?
    private var headerHeightConstraint: Constraint?
    
    private let headerHeight: CGFloat = 250
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupSubviews()
        setupConstraints()
    }
    
    private func setupSubviews() {
        headerImageView.image = UIImage(named: "cover")
        headerImageView.contentMode = .scaleAspectFill
        headerImageView.clipsToBounds = true
        view.addSubview(headerImageView)
        
        tableView.delegate = self
        tableView.dataSource = self
        tableView.backgroundColor = .clear
        tableView.contentInset = UIEdgeInsets(top: headerHeight, left: 0, bottom: 0, right: 0)
        view.addSubview(tableView)
    }
    
    private func setupConstraints() {
        tableView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        
        headerImageView.snp.makeConstraints { make in
            make.leading.trailing.equalToSuperview()
            self.headerTopConstraint = make.top.equalToSuperview().constraint
            self.headerHeightConstraint = make.height.equalTo(headerHeight).constraint
        }
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let offsetY = scrollView.contentOffset.y
        if offsetY < -headerHeight {
            // Pastga tortilganda: cho'zish
            headerTopConstraint?.update(offset: 0)
            headerHeightConstraint?.update(offset: -offsetY)
        } else {
            // Tepaga scroll bo'lganda: qotirib qo'yish yoki scroll qilish
            headerTopConstraint?.update(offset: -(offsetY + headerHeight))
            headerHeightConstraint?.update(offset: headerHeight)
        }
    }
    
    // TableView data source stubs...
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 20 }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { return UITableViewCell() }
}

4. Custom TabBar (Bottom Navigation) {#custom-tabbar}

O'rtasida katta "qo'shish" tugmasi bor, ostida silliq siljiydigan indikator chizig'iga ega Custom TabBar.

import UIKit
import SnapKit

@MainActor
class CustomTabBar: UIView {
    var didSelectTab: ((Int) -> Void)?
    
    private let stackView = UIStackView()
    private let indicatorView = UIView()
    private let centerButton = UIButton(type: .system)
    
    private var indicatorLeadingConstraint: Constraint?
    private let tabCount: Int = 4
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupSubviews()
        setupConstraints()
    }
    
    required init?(coder: NSCoder) { fatalError() }
    
    private func setupSubviews() {
        backgroundColor = .white
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowOpacity = 0.1
        layer.shadowOffset = CGSize(width: 0, height: -2)
        
        stackView.axis = .horizontal
        stackView.distribution = .fillEqually
        addSubview(stackView)
        
        indicatorView.backgroundColor = .systemBlue
        addSubview(indicatorView)
        
        for i in 0..<tabCount {
            if i == 2 {
                // Dummy view for center button space
                let dummy = UIView()
                stackView.addArrangedSubview(dummy)
                continue
            }
            let btn = UIButton(type: .system)
            btn.tag = i
            btn.setImage(UIImage(systemName: "star.fill"), for: .normal)
            btn.addTarget(self, action: #selector(tabTapped(_:)), for: .touchUpInside)
            stackView.addArrangedSubview(btn)
        }
        
        centerButton.backgroundColor = .systemBlue
        centerButton.tintColor = .white
        centerButton.setImage(UIImage(systemName: "plus"), for: .normal)
        centerButton.layer.cornerRadius = 25
        addSubview(centerButton)
    }
    
    private func setupConstraints() {
        stackView.snp.makeConstraints { make in
            make.top.leading.trailing.equalToSuperview()
            make.bottom.equalTo(safeAreaLayoutGuide.snp.bottom)
            make.height.equalTo(60)
        }
        
        centerButton.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.centerY.equalTo(stackView.snp.top)
            make.size.equalTo(50)
        }
        
        indicatorView.snp.makeConstraints { make in
            make.bottom.equalTo(stackView.snp.bottom)
            make.height.equalTo(3)
            make.width.equalTo(snp.width).dividedBy(tabCount)
            self.indicatorLeadingConstraint = make.leading.equalToSuperview().constraint
        }
    }
    
    @objc private func tabTapped(_ sender: UIButton) {
        let index = sender.tag
        let tabWidth = bounds.width / CGFloat(tabCount)
        
        UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5) {
            self.indicatorLeadingConstraint?.update(offset: CGFloat(index) * tabWidth)
            self.layoutIfNeeded()
        }
        
        didSelectTab?(index)
    }
}

5. Draggable Bottom Sheet (Apple Maps uslubi) {#bottom-sheet}

BottomSheet 3 xil holatda bo'lishi mumkin: yopilgan, yarim va to'liq ochiq. PanGesture bilan boshqariladi.

import UIKit
import SnapKit

@MainActor
class BottomSheetViewController: UIViewController {
    private let sheetView = UIView()
    private let handleBar = UIView()
    private var topConstraint: Constraint?
    
    private let collapsedHeight: CGFloat = 100
    private let halfHeight: CGFloat = UIScreen.main.bounds.height / 2
    private let expandedHeight: CGFloat = 80
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupSubviews()
        setupConstraints()
    }
    
    private func setupSubviews() {
        sheetView.backgroundColor = .white
        sheetView.layer.cornerRadius = 20
        sheetView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
        sheetView.layer.shadowColor = UIColor.black.cgColor
        sheetView.layer.shadowOpacity = 0.2
        sheetView.layer.shadowRadius = 10
        view.addSubview(sheetView)
        
        handleBar.backgroundColor = .systemGray4
        handleBar.layer.cornerRadius = 3
        sheetView.addSubview(handleBar)
        
        let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
        sheetView.addGestureRecognizer(pan)
    }
    
    private func setupConstraints() {
        sheetView.snp.makeConstraints { make in
            make.leading.trailing.bottom.equalToSuperview()
            self.topConstraint = make.top.equalToSuperview().offset(view.bounds.height - collapsedHeight).constraint
        }
        
        handleBar.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(10)
            make.centerX.equalToSuperview()
            make.width.equalTo(40)
            make.height.equalTo(6)
        }
    }
    
    @objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
        let translation = gesture.translation(in: view)
        let velocity = gesture.velocity(in: view)
        
        switch gesture.state {
        case .changed:
            let currentOffset = topConstraint?.layoutConstraints.first?.constant ?? 0
            let newOffset = max(expandedHeight, currentOffset + translation.y)
            topConstraint?.update(offset: newOffset)
            gesture.setTranslation(.zero, in: view)
            
        case .ended:
            let currentOffset = topConstraint?.layoutConstraints.first?.constant ?? 0
            let targetOffset: CGFloat
            
            if velocity.y > 500 {
                targetOffset = view.bounds.height - collapsedHeight // Swipe down
            } else if velocity.y < -500 {
                targetOffset = expandedHeight // Swipe up
            } else {
                // Snap to nearest
                let diffCollapsed = abs(currentOffset - (view.bounds.height - collapsedHeight))
                let diffHalf = abs(currentOffset - halfHeight)
                let diffExpanded = abs(currentOffset - expandedHeight)
                
                let minDiff = min(diffCollapsed, min(diffHalf, diffExpanded))
                if minDiff == diffCollapsed { targetOffset = view.bounds.height - collapsedHeight }
                else if minDiff == diffHalf { targetOffset = halfHeight }
                else { targetOffset = expandedHeight }
            }
            
            UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: velocity.y / 1000) {
                self.topConstraint?.update(offset: targetOffset)
                self.view.layoutIfNeeded()
            }
        default: break
        }
    }
}

6. Animated Segmented Control {#animated-segmented}

Custom animatsiyalangan Segmented Control yaratish SnapKit constraint'larini o'zgartirish orqali judayam qulay amalga oshadi.

import UIKit
import SnapKit

@MainActor
class AnimatedSegmentedControl: UIView {
    private let segments: [String]
    private let stackView = UIStackView()
    private let indicatorView = UIView()
    private var indicatorLeadingConstraint: Constraint?
    
    init(segments: [String]) {
        self.segments = segments
        super.init(frame: .zero)
        setupSubviews()
        setupConstraints()
    }
    
    required init?(coder: NSCoder) { fatalError() }
    
    private func setupSubviews() {
        backgroundColor = .systemGray6
        layer.cornerRadius = 16
        
        indicatorView.backgroundColor = .white
        indicatorView.layer.cornerRadius = 14
        indicatorView.layer.shadowColor = UIColor.black.cgColor
        indicatorView.layer.shadowOpacity = 0.1
        indicatorView.layer.shadowRadius = 4
        indicatorView.layer.shadowOffset = CGSize(width: 0, height: 2)
        addSubview(indicatorView)
        
        stackView.axis = .horizontal
        stackView.distribution = .fillEqually
        addSubview(stackView)
        
        for (index, title) in segments.enumerated() {
            let button = UIButton(type: .system)
            button.setTitle(title, for: .normal)
            button.setTitleColor(.black, for: .normal)
            button.tag = index
            button.addTarget(self, action: #selector(segmentTapped), for: .touchUpInside)
            stackView.addArrangedSubview(button)
        }
    }
    
    private func setupConstraints() {
        stackView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        
        indicatorView.snp.makeConstraints { make in
            make.top.bottom.equalToSuperview().inset(2)
            make.width.equalTo(snp.width).dividedBy(segments.count).offset(-4)
            self.indicatorLeadingConstraint = make.leading.equalToSuperview().offset(2).constraint
        }
    }
    
    @objc private func segmentTapped(_ sender: UIButton) {
        let segmentWidth = bounds.width / CGFloat(segments.count)
        let newOffset = (segmentWidth * CGFloat(sender.tag)) + 2
        
        UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5) {
            self.indicatorLeadingConstraint?.update(offset: newOffset)
            self.layoutIfNeeded()
        }
    }
}

7. Circular Progress Ring (Apple Watch uslubi) {#progress-ring}

import UIKit
import SnapKit

@MainActor
class CircularProgressRing: UIView {
    private let trackLayer = CAShapeLayer()
    private let progressLayer = CAShapeLayer()
    private let percentLabel = UILabel()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }
    
    required init?(coder: NSCoder) { fatalError() }
    
    private func setupViews() {
        let path = UIBezierPath(arcCenter: CGPoint(x: 50, y: 50), radius: 40, startAngle: -.pi/2, endAngle: 1.5 * .pi, clockwise: true)
        
        trackLayer.path = path.cgPath
        trackLayer.strokeColor = UIColor.systemGray5.cgColor
        trackLayer.lineWidth = 10
        trackLayer.fillColor = UIColor.clear.cgColor
        layer.addSublayer(trackLayer)
        
        progressLayer.path = path.cgPath
        progressLayer.strokeColor = UIColor.systemBlue.cgColor
        progressLayer.lineWidth = 10
        progressLayer.fillColor = UIColor.clear.cgColor
        progressLayer.lineCap = .round
        progressLayer.strokeEnd = 0
        layer.addSublayer(progressLayer)
        
        percentLabel.font = .boldSystemFont(ofSize: 20)
        percentLabel.textAlignment = .center
        addSubview(percentLabel)
        
        percentLabel.snp.makeConstraints { make in
            make.center.equalToSuperview()
        }
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        // Markazni to'g'rilash
        let center = CGPoint(x: bounds.midX, y: bounds.midY)
        let radius = min(bounds.width, bounds.height) / 2 - 10
        let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: -.pi/2, endAngle: 1.5 * .pi, clockwise: true)
        trackLayer.path = path.cgPath
        progressLayer.path = path.cgPath
    }
    
    func setProgress(_ progress: CGFloat, animated: Bool) {
        let clamped = max(0, min(1, progress))
        percentLabel.text = "\(Int(clamped * 100))%"
        
        if animated {
            let animation = CABasicAnimation(keyPath: "strokeEnd")
            animation.toValue = clamped
            animation.duration = 1.0
            animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
            progressLayer.add(animation, forKey: "progressAnim")
        }
        progressLayer.strokeEnd = clamped
    }
}

8. Interactive Card Stack (Tinder uslubi) {#card-stack}

Karta stackini yaratib, eng yuqoridagisini harakatlantiramiz va aylanma (rotation) effektini beramiz.

import UIKit
import SnapKit

@MainActor
class CardStackView: UIView {
    private var cards: [UIView] = []
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupCards()
    }
    
    required init?(coder: NSCoder) { fatalError() }
    
    private func setupCards() {
        let colors: [UIColor] = [.systemRed, .systemGreen, .systemBlue]
        
        for (index, color) in colors.enumerated() {
            let card = UIView()
            card.backgroundColor = color
            card.layer.cornerRadius = 20
            card.layer.shadowOpacity = 0.2
            addSubview(card)
            
            card.snp.makeConstraints { make in
                make.center.equalToSuperview()
                make.size.equalTo(CGSize(width: 300, height: 400))
            }
            
            // Perspective transformation for cards behind
            if index < colors.count - 1 {
                let scale = 1.0 - (CGFloat(colors.count - 1 - index) * 0.05)
                let yOffset = CGFloat(colors.count - 1 - index) * 15
                card.transform = CGAffineTransform(scaleX: scale, y: scale).translatedBy(x: 0, y: yOffset)
            }
            
            cards.append(card)
        }
        
        let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
        cards.last?.addGestureRecognizer(pan)
        cards.last?.isUserInteractionEnabled = true
    }
    
    @objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
        guard let topCard = cards.last else { return }
        let translation = gesture.translation(in: self)
        
        switch gesture.state {
        case .changed:
            let rotation = (translation.x / bounds.width) * 0.5
            topCard.transform = CGAffineTransform(translationX: translation.x, y: translation.y).rotated(by: rotation)
        case .ended:
            if abs(translation.x) > 100 {
                // Swipe away
                UIView.animate(withDuration: 0.3) {
                    let endX = translation.x > 0 ? 500.0 : -500.0
                    topCard.transform = CGAffineTransform(translationX: endX, y: translation.y)
                    topCard.alpha = 0
                } completion: { _ in
                    topCard.removeFromSuperview()
                    self.cards.removeLast()
                    // Re-attach gesture to new top card
                    if let newTop = self.cards.last {
                        newTop.isUserInteractionEnabled = true
                        newTop.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan)))
                        UIView.animate(withDuration: 0.3) {
                            newTop.transform = .identity
                        }
                    }
                }
            } else {
                // Return to center
                UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.5) {
                    topCard.transform = .identity
                }
            }
        default: break
        }
    }
}

9. Custom Alert/Dialog {#custom-alert}

Apple'ning standart UIAlertController dizayni bilan cheklanib qolmaslik uchun maxsus alert view quramiz. Barcha ko'rinishlar SnapKit yordamida tez va silliq pozitsiyalanadi.

import UIKit
import SnapKit

@MainActor
class CustomAlertView: UIView {
    private let dimView = UIView()
    private let containerView = UIView()
    
    static func show(in viewController: UIViewController, title: String, message: String) {
        let alert = CustomAlertView(title: title, message: message)
        viewController.view.addSubview(alert)
        
        alert.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        
        alert.animateIn()
    }
    
    private init(title: String, message: String) {
        super.init(frame: .zero)
        
        dimView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
        dimView.alpha = 0
        addSubview(dimView)
        
        containerView.backgroundColor = .white
        containerView.layer.cornerRadius = 16
        containerView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
        containerView.alpha = 0
        addSubview(containerView)
        
        let titleLabel = UILabel()
        titleLabel.text = title
        titleLabel.font = .boldSystemFont(ofSize: 20)
        
        let messageLabel = UILabel()
        messageLabel.text = message
        messageLabel.numberOfLines = 0
        messageLabel.textAlignment = .center
        
        let okButton = UIButton(type: .system)
        okButton.setTitle("Tushunarli", for: .normal)
        okButton.backgroundColor = .systemBlue
        okButton.setTitleColor(.white, for: .normal)
        okButton.layer.cornerRadius = 10
        okButton.addTarget(self, action: #selector(dismiss), for: .touchUpInside)
        
        [titleLabel, messageLabel, okButton].forEach { containerView.addSubview($0) }
        
        dimView.snp.makeConstraints { make in make.edges.equalToSuperview() }
        
        containerView.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.width.equalTo(300)
        }
        
        titleLabel.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(20)
            make.centerX.equalToSuperview()
        }
        
        messageLabel.snp.makeConstraints { make in
            make.top.equalTo(titleLabel.snp.bottom).offset(10)
            make.leading.trailing.equalToSuperview().inset(20)
        }
        
        okButton.snp.makeConstraints { make in
            make.top.equalTo(messageLabel.snp.bottom).offset(20)
            make.leading.trailing.equalToSuperview().inset(20)
            make.bottom.equalToSuperview().offset(-20)
            make.height.equalTo(44)
        }
    }
    
    required init?(coder: NSCoder) { fatalError() }
    
    private func animateIn() {
        UIView.animate(withDuration: 0.3) {
            self.dimView.alpha = 1
        }
        UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5) {
            self.containerView.transform = .identity
            self.containerView.alpha = 1
        }
    }
    
    @objc private func dismiss() {
        UIView.animate(withDuration: 0.2, animations: {
            self.containerView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
            self.containerView.alpha = 0
            self.dimView.alpha = 0
        }) { _ in
            self.removeFromSuperview()
        }
    }
}

10. Performance Comparison {#performance}

Custom ViewSwiftUI qiyinligiSnapKit/UIKit qiyinligiPerformance (SwiftUI vs UIKit)
Interactive SliderYuqori (State + DragGesture qotadi)O'rta (Touch tracking talab etadi)UIKit ancha silliq va ishonchli.
Pinch-to-ZoomJuda yuqori (Chegaralarni nazorat qilish)Past (UIScrollView hammasini qiladi)UIKit to'liq g'olib (tekinga physics keladi).
Stretchy HeaderO'rta (GeometryReader ishlatiladi)O'rta (Delegate update qilinadi)UIKit frame laglarsiz silliq ishlaydi.
Bottom SheetPast (iOS 16+ da), Lekin cheklanganO'rta (PanGesture)SwiftUI customizatsiyada yutqazadi.
Card StackO'rta (ZStack + Gesture)O'rta (Transformations)Ikkisi ham yaxshi, biroq UIKit'da transform moslashuvchanroq.
Progress RingPast (Circle trim)O'rta (CAShapeLayer)Ikkisi ham juda tez.

Muhim: SwiftUI asosan deklarativ bo'lib, murakkab gesture-driven state o'zgarishlarida re-render tsikllarini keraksiz chaqirib yuborishi mumkin. UIKit esa to'g'ridan-to'g'ri layerni yoki constraintni yangilab, faqat o'sha qismni qayta chizadi.


11. Expert Tips {#expert-tips}

  • **hitTest(_:with:) orqali touch area kengaytirish:** Ba'zida kichik tugma yopilishini osonlashtirish uchun touch area ni ko'paytirish kerak bo'ladi. Frame'dan chiqib turgan qismlarga bosilganda ishlashi uchun hitTest ni override qiling.
  • **CADisplayLink bilan 120Hz animatsiya:** Animatsiya bloklari ba'zi maxsus fizikalarda ishlamasa, CADisplayLink orqali har bir ekranning yangilanish (refresh) tsiklida frame'larni hisoblang.
  • **UIViewPropertyAnimator bilan interruptible animatsiya:** Bottom sheet yoki slider kabi pan gesture ustiga qurilgan joylarda animatsiya to'xtatilib, davom ettirilishi mumkin bo'lishi kerak. Standard UIView.animate bu vazifani 100% bajara olmaydi.
  • Dark/Light Mode adaptatsiyasi: Ekranga CALayer kabi past darajadagi API'lar bilan chizilgan gradient va border ranglari theme o'zgarganda avtomatik yangilanmaydi. traitCollectionDidChange ichida layer.borderColor = UIColor.label.cgColor deb qo'lda yangilash yodingizdan chiqmasin!

Xulosa {#xulosa}

"SnapKit Expert Yo'l Xaritasi" shuni anglatadiki, faqatgina view'larni yonma-yon joylashtirishni bilish emas, balki foydalanuvchining har bir teginishiga (touch), surishiga (swipe) munosib javob qaytaradigan va ekranda sehrli tuyuladigan interfeyslar qura olishingiz kerak.

SwiftUI yangi texnologiya bo'lsa-da, katta korporativ loyihalarda aynan shu darsdagi kabi nozik UI elementlari haliyam UIKit + SnapKit'da yozilmoqda.

Manbalar {#manbalar}

Markdown manba

iOS Lessons