In questo post andremo ad approfondire una tecnologia spesso sottovalutata e poco conosciuta del framework UIKit, le animazioni o transizioni personalizzate tra controllers.

Per iniziare è bene aver presente alcuni protocolli e oggetti.

UIViewControllerAnimatedTransitioning

Questo è uno dei protocolli principali che regolano le transizioni tra controllers. Ci permette di regolare la durata della transizione e di animare i contenuti sia del controller di partenza, che di destinazione.

UIViewControllerTransitioningDelegate

Questo protocollo ci permette, invece, di applicare il nostro transitioning ai diversi controllers che vogliamo animare.

In questo primo articolo creeremo un’animazione non troppo complicata e che ho utilizzato in una mia app in fase di sviluppo.

Per prima cosa creiamo il nostro ComposerAnimator che conformiamo al protocollo UIViewControllerAnimatedTransitioning.

final class ComposerAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        
    }
    
}

Il metodo transitionDuration() ci permette di fornire la durata totale dell’animazione, mentre il metodo animateTransition è il metodo che andremo ad utilizzare per animare le transizioni.

In particolare il transitionContext ci permette di ottenere oggetti chiave come il controller o la view di partenza o destinazione.

Prima di modificare questi metodi aggiungiamo delle proprietà chiave al nostro animator:

  • duration la durata dell’animazione
  • mode un valore dell’enum PresentationMode che utilizzeremo per regolare le transizioni nel caso in cui il controller venga presentato o nascosto.
...

enum PresentationMode {
    case present
    case dismiss
}

private var mode: PresentationMode
private var duration: TimeInterval
    
init(presentationMode: PresentationMode, duration: TimeInterval) {
    self.mode = presentationMode
    self.duration = duration
}

...

Ora siamo pronti a creare la nostra animazione. Per prima cosa dobbiamo capire il modo in cui il nostro controller deve essere presentato e deve interagire con la view del controller di partenza. Ciò che vogliamo fare è presentare il controller in modo da creare una transizione dal basso verso l’alto, offuscando il controller di partenza con una view non completamente opaca.

Per prima cosa recuperiamo gli elementi di cui avremo bisogno dal context:

  • containerView la view che viene presentata durante la transizione tra in controller e in cui andremo ad agire
  • toView la view di destinazione
  • snapshot come indica il nome sarà una fotocopia della view di destinazione che andremo a modificare lasciando intatta la view originale
func animateTransitioning(using transitionContext: UIViewControllerContextTransitioning) {
    let containerView = transitionContext.containerView
        
    guard 
        let toView = transitionContext.view(forKey: .to), 
        let snapshot = toView.snapshotView(afterScreenUpdates: true) 
    else { return }        
        
}

Adesso applichiamo al nostro snapshot una traslazione verso il basso per prepararla all’animazione.

...

snapshot.transform = CGAffineTransform(translationX: 0, y: snapshot.frame.height)

...

Ora andremo a creare una view per sfocare la view di partenza per poi aggiungere tutte le view di cui abbiamo bisogno al containerView

...

// Creaiamo una view  a cui applichiamo un effetto sfocato
let effect = UIBlurEffect(style: .systemMaterialDark)
let blurView = UIVisualEffectView(effect: effect)
blurView.frame = containerView.bounds
// La rendiamo non visibile per poi animare il suo alpha successivamente
blurView.alpha = 0
// Aggiungiamo tutti gli elementi creati al nostro container
containerView.addSubview(blurView)
containerView.addSubview(snapshot)

...

Ora siamo pronti per animare i vari elementi. Ciò che andremo ad animare sarà la traslazione verso l’alto del nostro snapshot e l’opacità della nostra blurView.

...

UIView.animate(withDuration: duration, animations: {
    blurView.alpha = 0.2
    snapshot.transform = .identity
}) { (success) in
    snapshot.removeFromSuperview()
    containerView.addSubview(toView)
    transitionContext.completeTransition(success)
}

...

Al termine della nostra animazione aggiungiamo al nostro container la view originaria del controller di destinazione e comunichiamo al context di aver completato la nostra transizione.

Il codice utilizzato ci permette di animare la presentazione del controller. Se, invece, volessimo gestire il dismiss del controller dovremmo servirci di diversi metodi a seconda della presentazione.

Ci serviremo quindi di due metodi, uno da chiamare per presentare il controller, l’altro per nasconderlo.

private func present(using transitionContext: UIViewControllerContextTransitioning) {    
    ...    
}
    
private func dismiss(using transitionContext: UIViewControllerContextTransitioning) {
    ...
}

Per effettuare il dismiss andremo a ricreare l’animazione a ritroso per passare dal controller presentato(di destinazione) a quello originario

private func dismiss(using transitionContext: UIViewControllerContextTransitioning) {
        
    let containerView = transitionContext.containerView
        
    guard 
        let fromVC = transitionContext.viewController(forKey: .from), 
        let fromView = transitionContext.view(forKey: .from), 
        // Creiamo uno snapshot del nostro controller ormai presentato. 
        // Poichè stiamo effettuando l'animazione inversa dovremmo richiamare il controller con la key: .from
        let snapshot = fromView.snapshotView(afterScreenUpdates: true),
        // Recuperiamo la blurView tra le subviews del nostro containerView
        let blurView = containerView.subviews.first(where: { $0 is UIVisualEffectView}) 
    else { return }
        
    containerView.addSubview(snapshot)
    fromView.removeFromSuperview()
        
    UIView.animate(withDuration: duration, animations: {
        // Nascondiamo la nostra blurView
        blurView.alpha = 0
        // Trasliamo verso il basso lo snapshot del controller presentato in precedenza
        snapshot.transform = CGAffineTransform(translationX: 0, y: snapshot.frame.height)
    }) { (success) in
        snapshot.removeFromSuperview()
        blurView.removeFromSuperview()
        fromVC.removeFromParent()

        transitionContext.completeTransition(success)
        }
    }

Infine modifichiamo il metodo animateTransitioning per richiamare i diversi metodi nel caso in cui il controller venga presentato o nascosto

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        
    switch mode {
    case .present:
        present(using: transitionContext)
    case .dismiss:
        dismiss(using: transitionContext)
    }
        
}

Siamo al 90% dell’opera. Ora dobbiamo solamente creare il nostro UIViewControllerTransitioningDelegate per applicare la transizione personalizzata ormai creata.

final class ComposerTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
    
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        ...
    }
    
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        ...
    }
    
}

I due metodi ci permettono di fornire un UIViewControllerAnimatedTransitioning personalizzato per presentare o nascondere il controller a cui applicheremo questo delegate. Di conseguenza arriveremo a una soluzione del genere.

func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) ->   UIViewControllerAnimatedTransitioning? {
    let controller = ComposerAnimator(presentationMode: .present, duration: 0.6)
    return controller
}
    
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    let controller = ComposerAnimator(presentationMode: .dismiss, duration: 0.6)
    return controller
}

Ora non ci resta che applicare il delegate al controller che vogliamo animare.

...

let transitioningDelegate = ComposerTransitioningDelegate()
controller.transitioningDelegate = transitioningDelegate
// Bisogna impostare il valore su .custom per utilizzare il nostro transitioningDelegate  
controller.modalPresentationStyle = .custom

self.present(controller, animated: true, completion: nil)

...

Et voilà, siamo riusciti a creare una transizione personalizzata per animare i nostri controller in maniera semplice ed intuitiva. Il progetto completo si trova sul mio profilo GitHub. Se hai qualche dubbio puoi contattarmi tranquillamente su Twitter.

Spero che questo articolo ti sia piaciuto e… stay tuned per non perdere i nuovi.