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
- Custom UIControl subclass — Interactive Slider
- Pinch-to-Zoom Image Viewer
- Stretchy Header (Parallax)
- Custom TabBar (Bottom Navigation)
- Draggable Bottom Sheet (Apple Maps uslubi)
- Animated Segmented Control
- Circular Progress Ring (Apple Watch uslubi)
- Interactive Card Stack (Tinder uslubi)
- Custom Alert/Dialog
- Performance Comparison
- Expert Tips
- Xulosa
- 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 View | SwiftUI qiyinligi | SnapKit/UIKit qiyinligi | Performance (SwiftUI vs UIKit) |
|---|---|---|---|
| Interactive Slider | Yuqori (State + DragGesture qotadi) | O'rta (Touch tracking talab etadi) | UIKit ancha silliq va ishonchli. |
| Pinch-to-Zoom | Juda yuqori (Chegaralarni nazorat qilish) | Past (UIScrollView hammasini qiladi) | UIKit to'liq g'olib (tekinga physics keladi). |
| Stretchy Header | O'rta (GeometryReader ishlatiladi) | O'rta (Delegate update qilinadi) | UIKit frame laglarsiz silliq ishlaydi. |
| Bottom Sheet | Past (iOS 16+ da), Lekin cheklangan | O'rta (PanGesture) | SwiftUI customizatsiyada yutqazadi. |
| Card Stack | O'rta (ZStack + Gesture) | O'rta (Transformations) | Ikkisi ham yaxshi, biroq UIKit'da transform moslashuvchanroq. |
| Progress Ring | Past (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. - **
CADisplayLinkbilan 120Hz animatsiya:** Animatsiya bloklari ba'zi maxsus fizikalarda ishlamasa, CADisplayLink orqali har bir ekranning yangilanish (refresh) tsiklida frame'larni hisoblang. - **
UIViewPropertyAnimatorbilan interruptible animatsiya:** Bottom sheet yoki slider kabi pan gesture ustiga qurilgan joylarda animatsiya to'xtatilib, davom ettirilishi mumkin bo'lishi kerak. StandardUIView.animatebu 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.
traitCollectionDidChangeichidalayer.borderColor = UIColor.label.cgColordeb 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.