de Trevor Phillips
Table of Contents
Cum să implementați animații interactive cu UIViewPropertyAnimator de la Swift

Să-i abandonăm urâtul UIView.animate(…)
cod și să-i actualizăm, nu-i așa?
Aici ne vom scufunda într-un exemplu practic folosind Apple UIViewPropertyAnimator
pentru a crea animații fluide combinate cu interacțiunea utilizatorului.
Puteți verifica rezultatul final al acestor animații în aplicația gratuită Bloq, care folosește tehnicile descrise mai jos ca bază pentru joc.

fundal
Este o clasă introdusă în iOS 10 care oferă mai multe capacități decât tradițională UIView.animate(...)
funcții:
- Porniți, opriți, întrerupeți sau reluați animația în orice moment
- Adăugați blocuri de animație și blocuri de finalizare la animator în timpul liber
- Inversați animația în orice moment
- „Frecați” animația, adică setați programatic cât de departe ar trebui să fie chiar acum
Noțiuni de bază
În primul rând, vom defini un controler de vizualizare personalizat BlockViewController
care va reprezenta fiecare pătrat colorat din cadrul jocului. Notă: Nu includ cod pentru culori, colțuri rotunjite sau alte aspecte care nu sunt relevante pentru acest tutorial.
class BlockViewController: UIViewController { var startingXOffset: CGFloat = 0 var endingXOffset: CGFloat = 0 var startingYOffset: CGFloat = 0 var endingYOffset: CGFloat = 0
var topConstraint = NSLayoutConstraint() var leadingConstraint = NSLayoutConstraint()
var animationDirection: AnimationDirection = .undefined var isVerticalAnimation: Bool { return animationDirection == .up || animationDirection == .down } var transitionAnimator: UIViewPropertyAnimator? var animationProgress: CGFloat = 0}
Proprietatile topConstraint
și leftConstraint
definiți decalajul vizualizării controlerului de vizualizare din partea superioară și din stânga supravegherii sale (respectiv).
offset
proprietățile sunt utilizate de UIViewPropertyAnimator
pentru a determina unde ar trebui să înceapă animația și unde ar trebui să se termine. Deoarece blocurile din joc se pot deplasa atât la stânga / dreapta, cât și sus / jos, le definim pe ambele X
și Y
compensări.
De asemenea, avem o enumere simplă AnimationDirection
pentru a ajuta cu logica necesară animațiilor.
enum AnimationDirection: Int { case up, down, left, right, undefined}
Acum, în controlerul de vizualizare viewDidLoad()
funcție, putem configura constrângeri ceva de genul acesta:
topConstraint = view.topAnchor.constraint(equalTo: superview.topAnchor, constant: startingYOffset)
leadingConstraint = view.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: startingXOffset)
topConstraint.isActive = true
leadingConstraint.isActive = true
let recognizer = UIPanGestureRecognizer()
recognizer.addTarget(self, action: #selector(viewPanned(recognizer:))) // will be defined later!
view.addGestureRecognizer(recognizer)

Funcții de ajutor
Să configurăm, de asemenea, câteva funcții de „ajutor” care vor fi utile mai târziu. Următoarele funcții vor schimba valorile de compensare:
private func swapXConstraints() { let tmp = endingXOffset endingXOffset = startingXOffset startingXOffset = tmp}
private func swapYConstraints() { let tmp = endingYOffset endingYOffset = startingYOffset startingYOffset = tmp}
Acesta va fi util pentru resetarea animației:
private func nullifyAnimations() { transitionAnimator = nil animationDirection = .undefined}
Și aici avem o funcție de inversare a animatorului:
private func reverseAnimation() { guard let animator = transitionAnimator else { return } animator.isReversed = !animator.isReversed}
Dacă animația este deja alergând și isReversed
este true
, atunci știm că animația rulează în direcția inversată. Dacă animația este nu alergând și isReversed
este true
, animația va rula în direcția inversată la pornire.
În cele din urmă, această mică funcție necesită velocity
reprezentat ca un CGPoint
, și determină ce direcție, dacă există, ar trebui să meargă animația în funcție de dacă componenta x sau componenta y a vitezei este mai mare ca mărime:
private func directionFromVelocity(_ velocity: CGPoint) -> AnimationDirection { guard velocity != .zero else { return .undefined } let isVertical = abs(velocity.y) > abs(velocity.x) var derivedDirection: AnimationDirection = .undefined if isVertical { derivedDirection = velocity.y < 0 ? .up : .down } else { derivedDirection = velocity.x < 0 ? .left : .right } return derivedDirection}
Interacțiunea cu utilizatorul
Să trecem la lucrurile esențiale: animațiile și interacțiunea cu utilizatorul!
În viewDidLoad()
am atașat un dispozitiv de recunoaștere a gestului pan la BlockViewController
punctul de vedere. Acest recunoscător de gesturi apelează funcția viewPanned(recognizer: UIPanGestureRecognizer)
când starea sa se schimbă.
@objcfunc viewPanned(recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: animationProgress = transitionAnimator?.fractionComplete ?? 0 case .changed: didChangePan(recognizer: recognizer) // described below case .ended: didEndPan(recognizer: recognizer) // described below default: break }}
Amintiți-vă cum am menționat capacitatea de „spălare” a UIViewPropertyAnimator
? fractionComplete
proprietatea ne permite să ajungem și să stabilim cât de departe ar trebui să fie animatorul cu animațiile sale. Această valoare variază de la 0,0 la 1,0.

animationProgress
este capturat în recognizer.state = .began
pentru că este posibil să avem o situație arătată mai sus, în care un gest pan este inițiat la mijlocul animației. În acest caz, vrem să „prindem” animația în starea sa actuală. animationProgress
proprietatea este utilizată pentru a activa acest comportament de „capturare”.
Functia viewPanned(recognizer: UIPanGestureRecognizer)
descarcă cea mai mare parte a logicii sale în două funcții, descrise mai jos. Codul nostru va deveni puțin mai complex, așa că pentru o mai bună lizibilitate și evidențierea sintaxei, voi trece la Github Gists acum.
Comentariile descriu ce se întâmplă. Rețineți că de fapt începem animația (dacă nu există) când state
în viewPanned(recognizer: UIPanGestureRecognizer)
este changed
Decat began
. Acest lucru se datorează faptului că viteza când state = .began
este întotdeauna zero. Nu putem determina direcția animației până când viteza nu este zero, așadar așteptăm până state = .changed
pentru a începe animația.
Când sunăm transitionAnimator.continueAnimation(...)
practic spunem: „Bine animator, utilizatorul a terminat de interacționat, așa că du-te și termină afacerea ta acum!” Trecere nil
pentru parametrul de sincronizare și 0
pentru factorul de durată va nu face ca animația să se termine instantaneu. În continuare va anima fără probleme până la final.
Logica, explicată
La sfârșitul acestei funcții, vedeți isOpposite
variabilă și o oarecare logică confuză în ceea ce privește animator.isReversed
? Să înțelegem ce se întâmplă aici.
private func oppositeOfInitialAnimation(velocity: CGPoint) -> Bool { switch animationDirection { case .up: return velocity.y > 0 case .down: return velocity.y < 0 case .left: return velocity.x > 0 case .right: return velocity.x < 0 case .undefined: return false }}
Variabila isOpposite
folosește funcția de ajutor de mai sus. Pur și simplu ia o viteză ca intrare și se întoarce true
dacă această viteză merge opus direcției curente de animație.
Apoi avem o declarație if-else cu două scenarii:
Cazul 1: Gestul panoramic sa încheiat în sens invers direcției sale inițiale, dar animatorul nu a fost inversat. Aceasta înseamnă că trebuie să inversăm animatorul înainte de a apela transitionAnimator.continueAnimation(...)
.
Cazul 2: Gestul panului s-a încheiat iniţială direcția, dar animatorul a fost inversat la un moment dat. Aceasta înseamnă că, din nou, trebuie să inversăm animatorul înainte de a apela transitionAnimator.continueAnimation(...)
.

Animaţie
În didChangePan(...)
am sunat beginAnimation()
dacă animatorul de tranziție a fost nil
. Iată implementarea pentru această funcție:
Lucrurile importante care se întâmplă sunt:
- Notificăm un delegat că ar trebui setat
startingXOffset
,endingXOffset
,startingYOffset
, șiendingYOffset
- Inițializăm
transitionAnimator
cu un bloc de animație care actualizează constrângerile vizualizării, apoi apelațilayoutIfNeeded()
- Configurăm blocul de finalizare al animatorului (descris mai jos)
- Dacă animația a fost inițiată programatic (nu este implicat niciun gest pan), apelăm
transitionAnimator.continueAnimation(...)
pentru a permite animației să se termine singură - Dacă animația a fost inițiată dintr-un gest panoramic, noi imediat pauză animația, mai degrabă decât să-i permită finalizarea. Acest lucru se datorează faptului că progresul animației va fi eliminat
didChangePan(...)
Finalizare animație
Ultima funcție care trebuie abordată este configureAnimationCompletionBlock()
, descris mai jos:
Dacă animatorul a terminat de unde a început, vom reseta constrângerile la modul în care erau înainte de animație.
Dacă animatorul a terminat așa cum era de așteptat, într-o poziție diferită, schimbăm constrângerile. Acest lucru permite vizualizarea să fie animată înainte și înapoi, iar și iar.

În cele din urmă, facem o verificare rapidă a sănătății pentru a ne asigura că dacă position
starea este .end
, vizualizarea a schimbat de fapt poziția. Când am dezvoltat aplicația, am avut un comportament de tip buggy, dar acest lucru a rezolvat problema.
rezumat
Eșantionul BlockViewController
cod pot fi găsite aici, dar vă rugăm să rețineți că este scos din context dintr-o aplicație mai mare. Nu va funcționa din cutie.
Pentru proiecte mai interesante, de la Node.js la Raspberry Pi, vă rugăm să nu ezitați vezi site-ul meu web. Sau descărcați Bloq gratuit pe App Store.