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
- Constraint-based animatsiya asosi
- SnapKit Constraint Reference bilan animatsiya
- remakeConstraints bilan to'liq layout almashish
- Amaliy loyiha 1: Accordion (Yig'ilib-ochiluvchi panel)
- Amaliy loyiha 2: Floating Action Button (FAB) menyu
- Amaliy loyiha 3: Stretchy Header (Tortiluvchi sarlavha)
- 120Hz ProMotion va animatsiya performance
- UIViewPropertyAnimator bilan interaktiv animatsiya
- Xulosa
- 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:
- Constraint'ni o'zgartirish (animatsiya blokidan TASHQARIDA).
- **
layoutIfNeeded()ni chaqirish** (UIView.animatebloki 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? ChunkitopConstraint?.updatefaqatgina 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)—offsetqiymatini o'zgartiradi (o'ngga/pastga/kattalashish musbat).update(inset: 20)—insetqiymatini o'zgartiradi (ichkariga qisish).- Agar boshqa elementga bog'liqlik o'zgarsa (masalan
topdanbottomga o'tish),remakeConstraintskerak 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):
- To'liq View'ni layout qilmang:
self.view.layoutIfNeeded()o'rniga, faqat kerakli qutinicontainerView.layoutIfNeeded()deb yangilang. Bu hisoblashlarni kamaytiradi. - Jiddiy arxitektura o'zgarishlari (masalan 100 ta cellli TableView ichidagi element) animatsiyasi qotishi mumkin, bunda
remakeConstraintsemas, 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 (
updateyokiremake) 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 yokiUIViewPropertyAnimatorvafractionCompletebilan bog'lash juda kuchli vositadir.