Анимация переходов ViewController или Animated Transitions in Swift

В предыдущих статьях мы рассмотрели возможности и некоторые простые методы анимации в Swift. В данном уроке мы рассмотрим пример использования анимации UIView.animateWithDuration,  расширим эти методы, чтобы создать свои собственные анимированные переходы (transition animations), как этот 

TransitionAnimation_custom_104_proSwiftRu

Следует сказать, что при разработке можно использовать массу стандартных методов анимации, которые по умолчанию обеспечивает Apple и Xcode. Однако, если вы хотите чтобы ваше приложение было оригинальным, — следует добавлять элементы индивидуальности, такие как этот анимированный переход.  

Создание проекта

Начнем как обычно с нуля, и далее создадим два контроллера, между которыми будем совершать переходы. Эти переходы будут запускаться по нажатию кнопок на этих контроллерах.

1. Создайте новый проект

Используйте шаблон ‘iOS Single View Application’, который автоматически создаст проект с одним UIViewController.

2. Добавьте кнопку

Перетащите кнопку на ViewController из библиотеки объектов и установите на ней надпись «Показать». Не забудьте добавить ограничения (constraints) так, чтобы кнопка всегда отображалась по центру в нижней части экрана на всех устройствах. С помощью функции Pin Autolayout зафиксируйте нижний, левый и правый края кнопки к краям экрана на расстоянии по 20 пунктов. Обновите рамку для кнопки, для удаления проблем совместимости Autolayout. Для удобства повествования, установите во вкладке Xcode Atribute Insprctor  серый цвет фона для контроллера ViewController.

3. Скопируйте view controller

Как и в любом другом редакторе, в Xcode можно  нажать ⌘-C, чтобы скопировать и ⌘-V, чтобы вставить объекты в Storyboard.

4. Настройте скопированный viewController

Изменение текст кнопки на «Убрать», цвет фона контроллера на, допустим, белый и цвет кнопки на какой нибудь контрастный.

5. Создайте переход (Segue) между экранами

Выполните Ctrl-перетягивание от кнопки на сером контроллере на белый контроллер  и отпустите, чтобы создать переход (Segue).

В открывшемся меню выберите тип перехода «Show».

TransitionAnimation_custom_04_proSwiftRu

6. Запустите проект

Того что мы сделали будет достаточно для запуска проекта, и по кнопке «Показать» наш серый контроллер совершает переход к белому UIViewController.  Т.к. мы не указали никакой информации об анимации перехода, iOS по умолчанию добавила  анимацию скольжения экрана снизу вверх.

TransitionAnimation_custom_05_proSwiftRu

Нажатие на кнопку «Убрать» мы никак не обрабатывали, поэтому скрыть белый контроллер не получается. Так что следующий шаг заключается в том, чтобы это исправить.

Мы уже разбирали иерархию контроллеров в статье Unwind​ S​egue. Как мы помним, все контроллеры в приложенгии можно представить как стек карт, и принято, что в этом стеке каждая карта презентует (presented) следующую. Т.е. при переходе в другой контроллер мы не заменяем текущий контроллер на новый а просто добавляем следующий контроллер в стек, в котором видно только последний контроллер.

Для чего это все тут написано? А для того, что бы вы понимали, что по кнопке «Показать» iOS приложение создает наш белый контроллер и показывает его поверх серого. Если на кнопку «Убрать» мы запрограммируем такой-же переход как из серого в белый контроллер только в обратном направлении, то серый контроллер у нас появится, однако это будет не тот первоначальный контроллер а вновь созданный экземпляр серого контроллера.  Он будет отображен поверх белого контроллера, который в свою очередь отображается поверх ИСХОДНОГО серого контроллера.

Т.е. если у нас в программе переходы будут организованы именно таким образом, то пользователь, выполнив достаточное число переходов от одного контроллера к другому,  забьет память устройства стеком из этих контроллеров. В конце концов, если переполнение памяти не обрабатывается отдельно, — приложение упадет, потому что каждому контроллеру требуется определенный объем памяти, а количество памяти для использования всегда ограничено.

Наша задача вместо выполнения регулярного перехода (Segue) вернутся к ранее созданному viewController.  Нам поможет уже известный нам Unwind​ S​egue.

7. Создайте Unwind Segue

Как вы помните, для создания Unwind Segue нужно в контроллере, в который нужно выполнить ​Unwind​ переход создать точку выхода: метод, помеченный ​@IBAction и он должен иметь единственный аргумент типа U​IStoryboardSegue. Добавьте метод в класс ViewController

Вот метод, который написал я:

//
// proSwift.ru
//
@IBAction func unwindToViewController (sender: UIStoryboardSegue){

}

Обратите внимание, что для выполнения перехода (unwind segue) методу  достаточно просто существовать. Тело данного метода можно оставить пустым. Далее выполните Ctrl-перетягивание от кнопки «Убрать» на белом контроллере на значок Exit вверху контроллера. В открывшемся меню выберите точку выхода (имя метода, который был создан только что).

TransitionAnimation_custom_06_proSwiftRu

8. Запустите проект еще раз

Убедитесь, что теперь по нажатию кнопки «Убрать» из белого контроллера, который находится на вершине стека,  этот белый viewController уходит с экрана и вы видите тот контроллер, который был ниже, — т.е. серый контроллер.

TransitionAnimation_custom_07_proSwiftRu

Давайте взглянем на наш проект. У нас есть простое приложение с переходами между двумя экранами, в которых установлена анимация «по умолчанию». Следующий шаг — это создание custom Transition Animation.

Настройка Transition Animations

Для выполнения анимированного перехода (Transition Animations) мы создадим объект «аниматор», который будет отвечать за тип анимации, которую мы хотим создать и применить. После создания мы должны указать iOS, что анимация именно этого объекта  «аниматор» должна быть использована вместо перехода по умолчанию.

Обе эти задачи решаются с помощью шаблона объектно-ориентированного программирования — протоколов.

Чтобы создать сам «аниматор» нам нужен объект, который соответствует протоколу UIViewControllerAnimatedTransitioning. Для замены значений по умолчанию и указания iOS, когда должен использоваться нужный нам объект, он  также должен соответствует протоколу UIViewControllerTransitioningDelegate.

Мы создадим объект, который будет отвечать требованиям обоих протоколов,  и называем его TransitionManager.

1. Создайте класс TransitionManager

Создайте новый файл в проекте, нажав ⌘-N и выбрав «Cocoa Touch Class» в первом диалоговом окне.  Введите имя класса TransitionManager и  укажите наследование класса от NSObject.

Это создаст новый Swift файл, который выглядит как что-то так:

//
// proSwift.ru 
// TransitionManager.swift
//  

import UIKit

class TransitionManager: NSObject {
   
}

2. Приведите класс к соответствию протоколам

После двоеточия в строке объявления класса через запятую укажите названия двух протоколов, указанных выше. Для соответствия нашего класса TransitionManager этим двум протоколам необходимо в класс добавить реализацию в общей сложности четырех методов.

Первые два метода необходимы для соответствия требованиям UIViewControllerAnimatedTransitioning, которые и делают этот класс «аниматором»:

  • animateTransition непосредственно определяет анимацию перехода от одного контроллера к другому.
  • transitionDuration возвращает количество секунд, которое занимает анимация

Последние два метода необходимы для соответствия требованиям UIViewControllerTransitioningDelegate,  и они будут предоставлять для iOS анимоторов, которые система будет использовать для представления контроллера на экран или для скрытия его с экрана:

  • animationControllerForPresentedController
  • animationControllerForDismissedController

Вот как выглядит класс TransitionManager после добавления соответствий протоколам:

//
// proSwift.ru
// TransitionManager.swift
//

import UIKit

class TransitionManager: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate  {
    
    // MARK: методы протокола UIViewControllerAnimatedTransitioning
    
    // метод, в котором непосредственно указывается анимация перехода от одного  viewcontroller к другому
    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        // код анимации
        
    }
    
    // метод возвращает количество секунд, которые длится анимация
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        return 0.6
    }
    
    // MARK: методы протокола UIViewControllerTransitioningDelegate
    
    // аниматор для презентации viewcontroller
    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return self
    }
    
    // аниматор для скрытия viewcontroller
    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return self
    }
    
}

Тут следует понимать, что «аниматор» (или контроллер анимации) это объект, который соответсвует протоколу UIViewControllerAnimatedTransitioning. А наш класс TrasitionManager ему соответствует. Поэтому в двух последних методах мы возвращаем self, т.е. наш класс.

3. Реализуйте  метод animateTransition

Как вы наверное уже поняли весь код описывающий анимацию будет именно в этом методе. Давайте для начала сделаем анимацию перехода, в которой новый контроллер слайдом выезжает справа, а текущий контроллер сдвигается влево.

Для такой реализации метод animateTransition выглядит следующим образом:

//
// proSwift.ru
//

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    // код анимации
    // 1
    let container = transitionContext.containerView()!
    let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
    let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
        
    // 2
    let offScreenRight = CGAffineTransformMakeTranslation(container.frame.width, 0)
    let offScreenLeft = CGAffineTransformMakeTranslation(-container.frame.width, 0)
        
    // 3
    toView.transform = offScreenRight
        
    // 4
    container.addSubview(toView)
    container.addSubview(fromView)
        
    // 5
    let duration = self.transitionDuration(transitionContext)
        
    // 6
    UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 0.49, initialSpringVelocity: 0.81, options: [], animations: { () -> Void in
        fromView.transform = offScreenLeft
        toView.transform = CGAffineTransformIdentity
        }) { (finished) -> Void in
            // 7
            transitionContext.completeTransition(true)
    }
}

Разберем все по-порядку

  1. Когда метод animateTransition выполняется, iOS передает в него объект, называемый transitionContext. Этот объект позволяет нам получить ссылки на view контроллер, с которого мы переходим и на который совершаем переход.  Также очень важно, что этот объект позволяет получить ссылку на третий view, который выступает как контейнер в котором должна быть выполнена анимация.
  2. Устанавливаем значения для 2D анимации. В переменные offScreenRight и offScreenLeft мы установили сдвиг вправо и влево соответственно на величину ширины container. Простыми словами — это сдвиг вправо и влево не величину экрана. Тут главное не запутаться: мы сделал сдвиги, но еще никому их не присвоили!
  3. А вот тут присваиваем сдвиг вправо для view в которое выполняем переход. Тут мы задали начальные позиции для всех view, с которых начнется анимация.
  4. Добавляем оба view в container. Мы должны вручную добавить fromView и toView ко view контейнера. Порядок добавления определяет что будет отображаться выше/над,  а что ниже/под, если оба view будут перекрываются во время анимации. Еще раз — в контейнере в пределах видимости — fromView и сдвинутое вправо toView — с этой позиции начинается анимация.
  5. Получаем продолжительность анимации. Не пишите вручную let duration = 0,6, хотя это и будет работать правильно. У нас есть метод, который вернет нам нужное время.
  6. Выполняем анимацию.  Во время анимации мы одновременно сдвигаем fromView и toView влево, таким образом  fromView сдвигается за экран, а toView выплывает в область видимости. Тут был использован метод usingSpringWithDamping для добавления небольшого отскока.
  7. В блоке завершения говорим нашему контексту transitionContext, что анимацию мы завершили.

4. Пробный запуск

Прежде чем мы запустить проект, мы должны обновить наш класс ViewController и создать экземпляр нашего TransitionManager.

Добавьте свойство выше функции viewDidLoad

//
// proSwift.ru
//

let transitionManager = TransitionManager()

И добавить метод prepareForSegue который вызывается, когда iOS готовится к переходу с одного экрана на другой. Это хороший момент, чтобы указать наш TransitionManager в качестве менеджера перехода вместо анимации перехода по умолчанию.

//
// proSwift.ru
//

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

    // 1 
    let toViewController = segue.destinationViewController as UIViewController
        
    // 2 
    toViewController.transitioningDelegate = self.transitionManager
}

  1. Получаем ссылку на контроллер, в который совершаем переход
  2. Вместо анимации перехода по умолчанию задаем наш transitionManager  в качестве объекта для управления анимацией перехода.

Теперь при переходе у нас должна работать анимация сдвига контроллеров вместо слайд перехода по умолчанию. Однако мы видим одинаковую анимацию как для показа контроллера так и для удаления его с экрана.

TransitionAnimation_custom_09_proSwiftRu

5. Разворот анимации

Чтобы изменить переход, мы просто должны добавить немного логики для нашего TransitionManager, чтобы он отслеживал появление контроллера или его удаление с экрана и подставлял соответствующую анимацию.

Добавьте свойство нашему классу  TransitionManager.

//
// proSwift.ru
//
var presenting = true

В методах animationControllerForPresentedController и  animationControllerForDismissedController установите соответствующие значения этому свойству.

//
// proSwift.ru
//
// MARK: методы протокола UIViewControllerTransitioningDelegate
    
    // аниматор для презентации viewController
    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        self.presenting = true
        return self
    }
    
    // аниматор для скрытия viewController
    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        self.presenting = false
        return self
    }

Затем в методе animateTransition следует проверить значение этого свойства и в соответствии с ним установить начальное расположения экранов для анимации.

//
// proSwift.ru
//

    // метод, в котором непосредственно указывается анимация перехода от одного  viewcontroller к другому
    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        // код анимации
        
        // 1
        let container = transitionContext.containerView()!
        let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
        let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
        
        // 2
        let offScreenRight = CGAffineTransformMakeTranslation(container.frame.width, 0)
        let offScreenLeft = CGAffineTransformMakeTranslation(-container.frame.width, 0)
        
        // 3
        if self.presenting {
            toView.transform = offScreenRight
        } else {
            toView.transform = offScreenLeft
        }
        
        // 4
        container.addSubview(toView)
        container.addSubview(fromView)
        
        // 5
        let duration = self.transitionDuration(transitionContext)
        
        // 6
        UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 0.49, initialSpringVelocity: 0.81, options: [], animations: { () -> Void in
            
            if self.presenting {
                fromView.transform = offScreenLeft
            } else {
                fromView.transform = offScreenRight
            }

            toView.transform = CGAffineTransformIdentity
            
            
            }) { (finished) -> Void in
                // 7
                transitionContext.completeTransition(true)
        }
    }

От предыдущей реализации этот метод отличается пунктом 3 и 6, в которых мы добавили проверку на свойство presenting, благодаря которому мы отслеживаем логику очередности появления контроллеров.

Запустите проект и вы увидите, что теперь экраны «выталкивают» друг друга с разных сторон.

TransitionAnimation_custom_101_proSwiftRu

6. Дорабатываем анимацию

Вся настройка анимации перехода (transition animate) находится в методе animateTransition. Поэтому всю доработку мы будем делать именно в этом методе. Нам необходимо задать начальный поворот на 90 градусов одного экрана и финальный поворот на 90 градусов другого экрана. Добавьте этот код к созданному методу.

//
// proSwift.ru
//


// 1
let π = CGFloat(M_PI)

// 2
let offScreenRotateIn = CGAffineTransformMakeRotation(-π/2)
let offScreenRotateOut = CGAffineTransformMakeRotation(π/2)

// 3
toView.transform = self.presenting ? offScreenRotateIn : offScreenRotateOut

// 4
toView.layer.anchorPoint = CGPoint(x:0, y:0)
fromView.layer.anchorPoint = CGPoint(x:0, y:0)

// 5
toView.layer.position = CGPoint(x:0, y:0)
fromView.layer.position = CGPoint(x:0, y:0)

Разберем по пунктам

  1. Устанавливаем константу π. Чтобы вывести символ «π» нужно нажать alt-P.
  2. Инициализируем повороты на 90 и -90 градусов. Как и в прошлый раз этот только экземпляры поворотов и они еще не присвоены никаким view
  3. А вот тут присваиваем поворот нашему toView, в зависимости от того будем его выводить на экран или убирать.
  4. Устанавливаем anchor point в левый верхний угол, чтобы вращать именно вокруг него.
  5. Раз уж изменили anchor point для вращения, нужно изменить и позицию view, т.к. их положение тоже отсчитывает от этой anchor point.

Чтобы не запутаться, приведу весь текст доработанного метода animateTransition. Теперь он выглядит следующим образом:

//
// proSwift.ru
//

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        // код анимации
        
        // 1
        let container = transitionContext.containerView()!
        let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
        let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
        
        // 1
        let π = CGFloat(M_PI)
        
        // 2
        let offScreenRotateIn = CGAffineTransformMakeRotation(-π/2)
        let offScreenRotateOut = CGAffineTransformMakeRotation(π/2)
        
        // 3
        toView.transform = self.presenting ? offScreenRotateIn : offScreenRotateOut
        
        // 4
        toView.layer.anchorPoint = CGPoint(x:0, y:0)
        fromView.layer.anchorPoint = CGPoint(x:0, y:0)
        
        // 5
        toView.layer.position = CGPoint(x:0, y:0)
        fromView.layer.position = CGPoint(x:0, y:0)
        

        container.addSubview(toView)
        container.addSubview(fromView)
        
        // 5
        let duration = self.transitionDuration(transitionContext)
        
        // 6
        UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 0.49, initialSpringVelocity: 0.81, options: [], animations: { () -> Void in
            
            if self.presenting {
                fromView.transform = offScreenRotateOut
            } else {
                fromView.transform = offScreenRotateIn
            }

            toView.transform = CGAffineTransformIdentity
            
            
            }) { (finished) -> Void in
                // 7
                transitionContext.completeTransition(true)
        }
    }

 

Запустите проект, и вы увидите анимацию вращения от одного экрана к другому.

TransitionAnimation_custom_102_proSwiftRu

Обратите внимание, что при выполнении анимации перехода (Transition animation) есть момент, когда видна лишь небольшая часть каждого экрана, и мы видим черный фон под ними. Это цвет фона по умолчанию containerView и он может быть изменен ​​при необходимости.

7. Обновление стиля статус бара

Последнее, что осталось сделать, — это переопределить метод preferredStatusBarStyle в нашем классе ViewController так, чтобы стиль статус бара выбирался в зависимости от цвета фона. Выбор стиля будет определятся той же логикой, которая определяет выводим мы экран или убираем.

//
// proSwift.ru
//

override func preferredStatusBarStyle() -> UIStatusBarStyle {
    return self.presentingViewController == nil ? UIStatusBarStyle.LightContent : UIStatusBarStyle.Default
}

 

TransitionAnimation_custom_103_proSwiftRu

8. Итог

Этот пример даст вам хорошую отправную точку для создания собственных пользовательских анимированных переходов, главное понимать принцип. А он следующий: вы создаете начальное расположение контроллеров относительно друг друга и контейнера в котором происходит анимация и вызываете  animateWithDuration к конечному расположению контроллеров.

Ждем комментариев и критики.

4 Comments on “Анимация переходов ViewController или Animated Transitions in Swift

  1. А как можно реализовать переход к другому виду без кнопок.
    Т.е. как запустить переход из тела обычной функции.

    • Можно реализовать различными способами.
      Вот тут пример как это сделать, работая непосредственно с контроллерами.
      Popover Controller на iPhone
      Смотрите часть про UITapGestureRecognizer. Там есть строка
      presentViewController(fvtppc, animated: true, completion: nil)
      Это то что Вам нужно.
      В этом случае Вам даже не нужен Storyboard

      — —

      Другой вариант это программно вызвать переход segue.
      Но тут нужно нарисовать в Storyboard все представления и переходы, задать имя для перехода и вызвать метод
      performSegueWithIdentifier(sender:)
      передав в этот метод имя перехода, заданное ранее в Storyboard

Добавить комментарий для admin Отменить ответ

Ваш адрес email не будет опубликован.

*

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.