# 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](#asosi)
2. [SnapKit Constraint Reference bilan animatsiya](#reference)
3. [remakeConstraints bilan to'liq layout almashish](#remake)
4. [Amaliy loyiha 1: Accordion (Yig'ilib-ochiluvchi panel)](#accordion)
5. [Amaliy loyiha 2: Floating Action Button (FAB) menyu](#fab)
6. [Amaliy loyiha 3: Stretchy Header (Tortiluvchi sarlavha)](#stretchy)
7. [120Hz ProMotion va animatsiya performance](#promotion)
8. [UIViewPropertyAnimator bilan interaktiv animatsiya](#animator)
9. [Xulosa](#xulosa)
10. [Manbalar](#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).

```swift
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.

```swift
// 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.

```text
Holat 1 (Ixcham):                  Holat 2 (Kengaytirilgan):
+--------------------+            +--------------------+
| [  Rasmiy  ] Text  |   ----->   | [     Rasm       ] |
|                    |            |                    |
+--------------------+            | [  Text to'liq   ] |
                                  +--------------------+
```

```swift
@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.

```swift
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.

```swift
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.

```swift
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:

```swift
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").

```swift
@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}
- [SnapKit rasmiy hujjatlari - Animations](http://snapkit.io/docs/)
- [Apple Developer: UIViewPropertyAnimator](https://developer.apple.com/documentation/uikit/uiviewpropertyanimator)
- [WWDC: Building Fluid Interfaces in iOS](https://developer.apple.com/videos/play/wwdc2018/803/)
