Azimjon Abdurasulov

SnapKit 8: SnapKit bilan Animatsiyalar — Constraint-Based Animation, layoutIfNeeded va 120Hz ProMotion

🧩 SnapKit — Zero to Hero

SnapKit 8: SnapKit bilan Animatsiyalar — Constraint-Based Animation, layoutIfNeeded va 120Hz ProMotion

Holat: 2026-yil oktabr. Interfeyslar jonli va silliq bo'lishi talab qilinadigan zamonda, constraint-based animatsiyalarni to'g'ri va samarali tushunish — 120Hz ProMotion ekranlarida ilovangizning ishlash tezligi (performance) va foydalanuvchiga beradigan hissiyotini belgilaydi.

Mundarija

  1. Constraint-based animatsiya asosi
  2. SnapKit Constraint Reference bilan animatsiya
  3. remakeConstraints bilan to'liq layout almashish
  4. Amaliy loyiha 1: Accordion (Yig'ilib-ochiluvchi panel)
  5. Amaliy loyiha 2: Floating Action Button (FAB) menyu
  6. Amaliy loyiha 3: Stretchy Header (Tortiluvchi sarlavha)
  7. 120Hz ProMotion va animatsiya performance
  8. UIViewPropertyAnimator bilan interaktiv animatsiya
  9. Xulosa
  10. Manbalar

1. Constraint-based animatsiya asosi {#asosi}

Auto Layout (SnapKit) ishlatganda elementlarni to'g'ridan-to'g'ri .frame orqali animatsiya qilib bo'lmaydi. Uning o'rniga, constraintlarni o'zgartirib, tizimga "yangi layoutni hisobla va shu jarayonni animatsiya qil" deb aytishimiz kerak.

Buning formulasi juda oddiy:

  1. Constraint'ni o'zgartirish (animatsiya blokidan TASHQARIDA).
  2. **layoutIfNeeded() ni chaqirish** (UIView.animate bloki ICHIDA).
import UIKit
import SnapKit

@MainActor
class BasicAnimationViewController: UIViewController {
    
    let boxView = UIView()
    var topConstraint: Constraint?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        boxView.backgroundColor = .systemBlue
        view.addSubview(boxView)
        
        boxView.snp.makeConstraints { make in
            make.width.height.equalTo(100)
            make.centerX.equalToSuperview()
            // Constraint reference'ni saqlaymiz
            self.topConstraint = make.top.equalTo(view.safeAreaLayoutGuide).offset(50).constraint
        }
    }
    
    @objc func moveBox() {
        // 1. Constraint'ni o'zgartirish (animatsiyadan TASHQARIDA)
        topConstraint?.update(offset: 200)
        
        // 2. Animatsiya bloki ichida yangi layoutni vizual ko'rsatish
        UIView.animate(withDuration: 0.35) {
            self.view.layoutIfNeeded()
        }
    }
}

Senior eslatmasi: Nima uchun layoutIfNeeded() animatsiya ichida yoziladi? Chunki topConstraint?.update faqatgina matematik modelni yangilaydi. layoutIfNeeded() esa shu matematik farqni (eski joyidan yangi joyiga) silliq o'tish bilan chizib beradi.


2. SnapKit Constraint Reference bilan animatsiya {#reference}

SnapKit-da constraintlarni o'zgartirishning eng tezkor yo'li — kerakli constraintni Constraint tipidagi o'zgaruvchida saqlash va update(offset:) chaqirishdir.

// Spring (kamon kabi) animatsiya bilan yangilash
UIView.animate(
    withDuration: 0.5,
    delay: 0,
    usingSpringWithDamping: 0.7,   // Prujina qattiqligi (0 dan 1 gacha)
    initialSpringVelocity: 0.5,    // Boshlang'ich tezlik
    options: .curveEaseInOut,
    animations: {
        self.view.layoutIfNeeded()
    }
)

Qachon update va qachon inset ishlatamiz?

  • update(offset: 20) — offset qiymatini o'zgartiradi (o'ngga/pastga/kattalashish musbat).
  • update(inset: 20) — inset qiymatini o'zgartiradi (ichkariga qisish).
  • Agar boshqa elementga bog'liqlik o'zgarsa (masalan top dan bottom ga o'tish), remakeConstraints kerak bo'ladi.

3. remakeConstraints bilan to'liq layout almashish {#remake}

Agar elementning nafaqat qiymatlari (offset), balki uning kimga bog'liqligi (anchorlari) o'zgarsa, to'liq constraintlarni o'chirib, qayta yaratadigan snp.remakeConstraints funksiyasidan foydalanamiz.

Holat 1 (Ixcham):                  Holat 2 (Kengaytirilgan):
+--------------------+            +--------------------+
| [  Rasmiy  ] Text  |   ----->   | [     Rasm       ] |
|                    |            |                    |
+--------------------+            | [  Text to'liq   ] |
                                  +--------------------+
@MainActor
func toggleCardState(isExpanded: Bool) {
    cardView.snp.remakeConstraints { make in
        if isExpanded {
            // Kengaytirilgan holat
            make.top.equalTo(view.safeAreaLayoutGuide).offset(20)
            make.leading.trailing.equalToSuperview().inset(20)
            make.height.equalTo(300)
        } else {
            // Ixcham holat
            make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-20)
            make.leading.trailing.equalToSuperview().inset(20)
            make.height.equalTo(80)
        }
    }
    
    UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0) {
        self.view.layoutIfNeeded()
    }
}

4. Amaliy loyiha 1: Accordion (Yig'ilib-ochiluvchi panel) {#accordion}

Accordion panellari foydalanuvchi ma'lumotlarini qismlarga bo'lib, ekranni tejashga yordam beradi.

import UIKit
import SnapKit

@MainActor
class AccordionViewController: UIViewController {
    
    let headerButton = UIButton(type: .system)
    let contentLabel = UILabel()
    let containerView = UIView()
    
    var isExpanded = false
    var contentHeightConstraint: Constraint?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        setupViews()
    }
    
    private func setupViews() {
        view.addSubview(containerView)
        containerView.backgroundColor = .secondarySystemBackground
        containerView.layer.cornerRadius = 12
        containerView.clipsToBounds = true
        
        containerView.addSubview(headerButton)
        headerButton.setTitle("Tafsilotlarni ko'rsatish", for: .normal)
        headerButton.titleLabel?.font = .boldSystemFont(ofSize: 16)
        headerButton.addTarget(self, action: #selector(toggleAccordion), for: .touchUpInside)
        
        containerView.addSubview(contentLabel)
        contentLabel.text = "SnapKit orqali qilingan ushbu animatsiya juda silliq va ishlashga qulay. Bu yerda siz xohlagancha tekst yozishingiz mumkin."
        contentLabel.numberOfLines = 0
        contentLabel.alpha = 0
        
        // Constraints
        containerView.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide).offset(40)
            make.leading.trailing.equalToSuperview().inset(20)
        }
        
        headerButton.snp.makeConstraints { make in
            make.top.leading.trailing.equalToSuperview()
            make.height.equalTo(50)
        }
        
        contentLabel.snp.makeConstraints { make in
            make.top.equalTo(headerButton.snp.bottom)
            make.leading.trailing.equalToSuperview().inset(16)
            self.contentHeightConstraint = make.height.equalTo(0).constraint
            make.bottom.equalToSuperview().offset(-16).priority(.low)
        }
    }
    
    @objc private func toggleAccordion() {
        isExpanded.toggle()
        
        let targetHeight = isExpanded ? contentLabel.intrinsicContentSize.height : 0
        contentHeightConstraint?.update(offset: targetHeight)
        
        headerButton.setTitle(isExpanded ? "Tafsilotlarni yashirish" : "Tafsilotlarni ko'rsatish", for: .normal)
        
        UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5) {
            self.contentLabel.alpha = self.isExpanded ? 1 : 0
            self.view.layoutIfNeeded()
        }
    }
}

5. Amaliy loyiha 2: Floating Action Button (FAB) menyu {#fab}

Bu loyihada asosiy tugma bosilganda qo'shimcha tugmalar (fan-out) ajralib chiqadi.

import UIKit
import SnapKit

@MainActor
class FABViewController: UIViewController {
    
    let mainFab = UIButton(type: .custom)
    let action1 = UIButton(type: .custom)
    let action2 = UIButton(type: .custom)
    
    var isMenuOpen = false
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemGroupedBackground
        setupFAB()
    }
    
    private func setupFAB() {
        [action1, action2, mainFab].forEach {
            $0.backgroundColor = .systemBlue
            $0.layer.cornerRadius = 28
            $0.tintColor = .white
            view.addSubview($0)
            
            $0.snp.makeConstraints { make in
                make.size.equalTo(56)
                // Default holatda hammasi mainFab ustida turadi
                make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-30)
                make.trailing.equalToSuperview().offset(-30)
            }
        }
        
        mainFab.setImage(UIImage(systemName: "plus"), for: .normal)
        action1.setImage(UIImage(systemName: "camera"), for: .normal)
        action2.setImage(UIImage(systemName: "photo"), for: .normal)
        
        action1.alpha = 0; action2.alpha = 0
        
        mainFab.addTarget(self, action: #selector(toggleMenu), for: .touchUpInside)
    }
    
    @objc private func toggleMenu() {
        isMenuOpen.toggle()
        
        // Kichik tugmalarning yangi pozitsiyasini o'rnatish
        action1.snp.remakeConstraints { make in
            make.size.equalTo(56)
            make.centerX.equalTo(mainFab)
            if isMenuOpen {
                make.bottom.equalTo(mainFab.snp.top).offset(-20) // Yuqoriga
            } else {
                make.bottom.equalTo(mainFab.snp.bottom)
            }
        }
        
        action2.snp.remakeConstraints { make in
            make.size.equalTo(56)
            make.centerY.equalTo(mainFab)
            if isMenuOpen {
                make.trailing.equalTo(mainFab.snp.leading).offset(-20) // Chapga
            } else {
                make.trailing.equalTo(mainFab.snp.trailing)
            }
        }
        
        UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0) {
            self.mainFab.transform = self.isMenuOpen ? CGAffineTransform(rotationAngle: .pi / 4) : .identity
            self.action1.alpha = self.isMenuOpen ? 1 : 0
            self.action2.alpha = self.isMenuOpen ? 1 : 0
            self.view.layoutIfNeeded()
        }
    }
}

6. Amaliy loyiha 3: Stretchy Header (Tortiluvchi sarlavha) {#stretchy}

Scroll pastga tortilganda rasm kattalashib (stretch), bo'shatilganda joyiga qaytadigan effekt.

import UIKit
import SnapKit

@MainActor
class StretchyHeaderController: UIViewController, UIScrollViewDelegate {
    
    let scrollView = UIScrollView()
    let contentView = UIView()
    let headerImageView = UIImageView()
    
    let headerMaxHeight: CGFloat = 250
    var headerHeightConstraint: Constraint?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupViews()
    }
    
    private func setupViews() {
        view.addSubview(scrollView)
        scrollView.delegate = self
        scrollView.alwaysBounceVertical = true
        
        scrollView.snp.makeConstraints { $0.edges.equalToSuperview() }
        
        scrollView.addSubview(contentView)
        contentView.snp.makeConstraints { make in
            make.edges.width.equalToSuperview()
            make.height.equalTo(1000) // Scroll bo'lishi uchun
        }
        
        headerImageView.image = UIImage(systemName: "photo.artframe")
        headerImageView.contentMode = .scaleAspectFill
        headerImageView.clipsToBounds = true
        headerImageView.backgroundColor = .systemIndigo
        
        view.addSubview(headerImageView)
        headerImageView.snp.makeConstraints { make in
            make.top.leading.trailing.equalToSuperview()
            self.headerHeightConstraint = make.height.equalTo(headerMaxHeight).constraint
        }
        
        // Scrollning ichki boshlanish joyini pastga suramiz
        scrollView.contentInset = UIEdgeInsets(top: headerMaxHeight, left: 0, bottom: 0, right: 0)
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let offsetY = scrollView.contentOffset.y
        
        // Agar offsetY manfiy bo'lsa (pastga tortilganda)
        if offsetY < -headerMaxHeight {
            // Qancha tortilgan bo'lsa, shuncha balandlik qo'shamiz
            let stretchAmount = abs(offsetY)
            headerHeightConstraint?.update(offset: stretchAmount)
        } else {
            // Normal holatda balandlik o'zgarmas qoladi
            headerHeightConstraint?.update(offset: headerMaxHeight)
        }
    }
}

Muhim: Stretchy header'da layoutIfNeeded() kerak emas! ScrollView delegate ichida constraintni update qilishining o'zi real vaqtda animatsiyasiz darhol yangilaydi (chunki user barmoq bilan harakatlantirmoqda).


7. 120Hz ProMotion va animatsiya performance {#promotion}

Zamonaviy iPhone'lar (Pro modellar) 120Hz ProMotion displeylariga ega. Bu degani animatsiyalar sekundiga 120 marta yangilanishi mumkin (120 fps).

Apple iOS 15 dan boshlab CADisplayLink va animatsiyalarni ProMotion uchun moslashtirdi. SnapKit orqali qilingan UIView.animate constraint o'zgarishlari avtomatik ravishda tizim darajasida 120Hz ga moslashadi.

Lekin siz aniq 120Hz talab qiluvchi animatsiya qilsangiz, CAAnimation orqali preferensiya berishingiz mumkin:

if #available(iOS 15.0, *) {
    // UIView animatsiyalari o'rniga ba'zan maxsus framerate talab etiladi
    let propertyAnimator = UIViewPropertyAnimator(duration: 0.5, curve: .easeInOut) {
        self.view.layoutIfNeeded()
    }
    // Apple avtomatik tarzda eng yaxshi frame rate'ni tanlaydi,
    // lekin core animation darajasida uni force qilish mumkin.
    propertyAnimator.startAnimation()
}

Constraint animatsiyalari bo'yicha maslahatlar (Performance):

  1. To'liq View'ni layout qilmang: self.view.layoutIfNeeded() o'rniga, faqat kerakli qutini containerView.layoutIfNeeded() deb yangilang. Bu hisoblashlarni kamaytiradi.
  2. Jiddiy arxitektura o'zgarishlari (masalan 100 ta cellli TableView ichidagi element) animatsiyasi qotishi mumkin, bunda remakeConstraints emas, shunchaki view-ni yashirish/ko'rsatish (alpha/isHidden) dan foydalanish tavsiya etiladi.

8. UIViewPropertyAnimator bilan interaktiv animatsiya {#animator}

SnapKit constraintlarini Pan Gesture (barmoq bilan siljitish) yordamida UIViewPropertyAnimator orqali to'liq nazorat qilish mumkin (misol uchun pastdan chiquvchi "Bottom Sheet" yoki "Drawer").

@MainActor
class DrawerViewController: UIViewController {
    
    let drawerView = UIView()
    var topConstraint: Constraint?
    var animator: UIViewPropertyAnimator!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        drawerView.backgroundColor = .systemPink
        drawerView.layer.cornerRadius = 20
        view.addSubview(drawerView)
        
        drawerView.snp.makeConstraints { make in
            make.leading.trailing.bottom.equalToSuperview()
            self.topConstraint = make.top.equalTo(view.snp.bottom).offset(-100).constraint
        }
        
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
        drawerView.addGestureRecognizer(panGesture)
    }
    
    @objc func handlePan(recognizer: UIPanGestureRecognizer) {
        switch recognizer.state {
        case .began:
            // Animatsiyani boshlaymiz, lekin darhol pauza qilamiz
            animator = UIViewPropertyAnimator(duration: 0.5, curve: .easeOut) {
                // Tepadagi joyni egallash uchun constraint'ni o'zgartiramiz
                self.topConstraint?.update(offset: -500)
                self.view.layoutIfNeeded()
            }
            animator.pauseAnimation()
            
        case .changed:
            let translation = recognizer.translation(in: view)
            // Barmoq harakatiga qarab progress'ni (0.0 dan 1.0 gacha) hisoblaymiz
            let fraction = -translation.y / 400
            animator.fractionComplete = max(0, min(1, fraction))
            
        case .ended:
            // Animatsiyani davom ettiramiz
            animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)
            
        default:
            break
        }
    }
}

Bu orqali animatsiyani barmog'ingiz bilan orqaga va oldinga qaytarishingiz mumkin!


Xulosa {#xulosa}

  • Constraint animatsiyasi ikki qadamdan iborat: modelni o'zgartirish (update yoki remake) va ekranga chizish (layoutIfNeeded + animate).
  • SnapKit Reference Constraint? obyektlarini saqlash orqali faqat kerakli qiymatlarni O(1) vaqtida yangilash imkonini beradi.
  • Interaktivlik uchun constraintlarni UIScrollView'ning offset'i bilan yoki UIViewPropertyAnimator va fractionComplete bilan bog'lash juda kuchli vositadir.

Manbalar {#manbalar}

Markdown manba

iOS Lessons