Базовая анимация UIView в Swift

Если вы нашли опечатку в тексте, выделите ее и нажмите CTRL + ENTER.

Лучшая часть анимации в iOS - это простота реализации! Вы должны написать буквально пару строчек кода и ваше приложение уже анимировано.

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

Сегодня вы узнаете как использовать базовую анимацию UIView API и цепочку анимаций.

Так что хватайте скорее свою корзинку для пикника и приступаем!

Поехали!

В скором времени вы поймете насколько несложно использование UIView анимации. Но вы должны держать в голове несколько шагов, которые позволят "оживить" ваши изображения на экране, если iOS не предоставляет вам встроенную поддержку анимации:

  • Спланируйте вызов метода для каждого элемента вашего приложения.
  • Каждому элементу посчитайте новое положение (x, y), основываясь на желаемом конечном положении, времени для запуска анимации и само время анимации.
  • Обновите позицию view (далее вид) на посчитанное значение, запустив желаемую анимацию.

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

Но не стоит переживать на этот счет, так как анимацию невероятно просто использовать в UIKit! Есть определенные свойства видов, такие как: размер, позиция, прозрачность, масштаб и т.д - они встроены в поддержку анимации. Вместо того, чтобы делать все шаги вручную мы просто:

  • Настроим блок анимации, определяя налету как долго должна длиться анимация и другие параметры.
  • Настроим анимируемые свойства вида внутри блока.

Вот и все, остальное возьмет на себя UIKit!

Давайте же скорее посмотрим на то, как будет выглядеть код анимируемой корзинки.

Открывание корзинки для пикника

Откройте Xcode и создайте новый проект File \ New Project. Выберите iOS \ Application \ Single View Application и нажмите Next. Назовите проект «Picnic» и выберите язык программирования Swift, для нашего device iPhone. Нажмите Next, выберите место для сохранения вашего проекта и нажмите Create.

Для этого проекта вам понадобится включить масштабирование для iPhone 6 и 6 Plus так, чтобы вам не пришлось переживать по поводу разных размеров телефонов. Вам необходимо удалить LaunchImage.xib, а также имя файла из Launch Screen File во вкладке General. Там же, найдите Launch Images Source и нажмите на кнопку Use Asset Catalog. После, выберите Images и в диалоговом окне нажмите Migrate.

Далее, скачайте некоторые картинки и звуки, которые вам пригодятся в проекте. Теперь, выберите в Xcode Images.xcassets, распакуйте скаченный архив, и перетащите туда все файлы из папки Images в открытый каталог.

Теперь, в навигаторе проекта нажмите на Main.storyboard, чтобы открыть редактор. Вы заметите, что вид представлен в виде квадрата, а не в виде какого-то конкретного устройства. Xcode 6 будет использовать по умолчанию классовые размеры, что означает, что один и тот же storyboard может быть использован для любых устройств, несмотря на размер экрана. Это достаточно крутая особенность, которая оптимизирует проект.

В выбранной Main.storyboard, откройте в правой части File Inspector. В секции Interface Builder Document есть чекбокс с Use Sizse Classes. Нажмите на него, чтобы он стал пустым. Проверьте, чтобы у вас был выбран iPhone в качестве устройства, когда вы отключаете Disable Size Classes.

Вам так же нужно отключить автопозиционирование, отключив чекбокс Use Auto Layout. Для простых приложений, автопозиционирование может создать вид более сложный, чем для того требуется. Мы не будем рассматривать его в этом туториале, но вы можете познакомиться с автопозиционированем поближе в "Автопозиционирование Часть 1: iOS 7."

Сейчас вам нужно настроить пользовательский интерфейс. Мы начнем с крышек корзины, которые будут открываться для отображения содержимого корзинки. Сперва перетащим View на основной вид и изменим его размер, чтобы он полностью заполнял вид родителя. Этот вид послужит нам контейнером для крышек корзинки. Затем, пройдите в Size Inspector и убедитесь, что значения положения x, y равны 0. Это будет означать, что вы расположили вид с начальной точки 0, 0.

Перетащите два UIImageView в вид, который является контейнером для крышек: один вид будет располагаться в верхней половине, второй - в нижней, и оба будут занимать примерно половину экрана. Выберите верхнюю картинку и идите в Attribute Inspector. Установите image door_top и установите View Mode на Bottom. По аналогии с нижней картинкой: устанавливаем image в door_bottom, и устанавливаем View Mode на Top. Потом меняем размеры картинок так, чтобы все выглядело прилично, как на рисунке ниже:

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

Время написать немного кода. Вам нужно объявить свойства для двух новых вводов для того, чтобы позднее мы могли анимировать. Откройте редактор и убедитесь, что вы выбрали ViewController.swift. Перетащите верхнюю картинку крышки с зажатым CTRL сразу под объявление класса и назовите этот UIImageView как basketTop. С нижней крышкой делаем то же самое, но называем как basketBottom. Вот что у вас должно получиться в результате:

class ViewController: UIViewController {
  @IBOutlet weak var basketTop: UIImageView!
  @IBOutlet weak var basketBottom: UIImageView!
  //...
}

Теперь у вас есть свойства, ссылающиеся на виды изображений, которые вы создали в Interface Builder. Так что теперь вы можете анимировать открывание корзинки, когда она только появляется. Переключитесь обратно в ViewController.swift и добавьте следующий код под viewDidLoad():

override func viewDidAppear(animated: Bool) {
  UIView.animateWithDuration(0.7, delay: 1.0, options: .CurveEaseOut, animations: {
    var basketTopFrame = self.basketTop.frame
    basketTopFrame.origin.y -= basketTopFrame.size.height
 
    var basketBottomFrame = self.basketBottom.frame
    basketBottomFrame.origin.y += basketBottomFrame.size.height
 
    self.basketTop.frame = basketTopFrame
    self.basketBottom.frame = basketBottomFrame
  }, completion: { finished in
    println("Basket doors opened!")
  })
}

Вызов animateWithDuration(delay:, options:, animations:, completion:) определяет длительность анимации в пол-секунды, которая начинается с задержкой в одну секунду. Анимация установлена как "ease out", что означает, что скорость снижается под конец анимации.

Блок animation определяет, какой будет анимация. У нас две границы двух видов, которые установлены в их финальное положение: верхняя крышка двигается наверх, нижняя - вниз, таки образом у нас получается эффект открытия корзинки. Так как вы установили продолжительность анимации, UIKit берет на себя анимирование открывания вашей корзинки.

Блок completion запускается после того, как анимация была исполнена или прервана, параметр finished имеет логический тип, который даст вам знать, закончилась анимация или нет.

Строим и запускаем наш код, и смотрим на наш изящно выполненный эффект. Достаточно просто для такого результата, не так ли?

Второй слой удовольствия

Когда вы идете на пикник, вы не просто кидаете еду в корзину, но и закрываете ее салфеткой от насекомых. Почему бы нам не повеселиться и не добавить анимации для нашего слоя с салфеткой?

Идем обратно в Main.storyboard и выбираем наши виды, с которыми мы только что работали. С выделенными слоями идем во вкладку меню Edit и нажимаем Dublicate. Теперь, разместите эти дублирующие слои поверх исходных, чтобы они перекрывали их.

Из Attribute Inspector установите: новое изображение, изображение топовое fabric_top, и новое нижнее изображение fabric_bottom.

Нам нужно, чтобы виды салфетки располагались под видами крышек корзинки. Откройте схему документа Editor \ Show Document Outline, если она еще не открыта. Так как документы считываются снизу вверх, то у вас должен получиться такой порядок:

  • fabric_top
  • fabric_bottom
  • basket_top
  • basket_bottom

Теперь, у вас все расположено в нужном порядке. А сможете ли вы сделать то же самое и с салфеткой, основываясь на примере с крышками корзинки? Смысл в том, чтобы убрать салфетки аналогично крышкам корзинки, но сделать это надо с небольшим запаздыванием от крышек. Двигаемся дальше, вы всегда сможете посмотреть решение позже, если вдруг у вас возникли сложности с выполнением.

Решение

В случае возникновения сложностей посмотрите на решение.

Сначала добавьте новое свойство в ViewController.swift перетаскиваем из storyboard в ваш ViewController.swift, как вы это делали с крышками корзинки:

@IBOutlet weak var fabricTop: UIImageView!
@IBOutlet weak var fabricBottom: UIImageView!

Добавьте следующий код внизу метода viewDidLoad():

UIView.animateWithDuration(1.0, delay: 1.2, options: .CurveEaseOut, animations: {
  var fabricTopFrame = self.fabricTop.frame
  fabricTopFrame.origin.y -= fabricTopFrame.size.height
 
  var fabricBottomFrame = self.fabricBottom.frame
  fabricBottomFrame.origin.y += fabricBottomFrame.size.height
 
  self.fabricTop.frame = fabricTopFrame
  self.fabricBottom.frame = fabricBottomFrame
}, completion: { finished in
  println("Napkins opened!")
})

Это совершенно незначительные изменения кода по сравнению с крышками от корзинки. Обратите внимание, что значения параметров продолжительности и задержки салфетки немного больше.

Запустите ваш код и вы увидите, что корзинка открывается в еще более эффектной форме.

Как соединять анимации

На этот момент мы анимировали единственное свойство UIView'ов - frame, также, вы закончили разовую анимацию. 

Однако, как было заявлено ранее, у нас есть несколько свойств, которые так же вызывают анимацию. Что ж, давайте проведем эксперимент с двумя интересными свойствами (center, transform) и используем анимационную связку!

Но сначала, давайте добавим еще кое-что в нашу корзинку для пикника! Откройте Main.storyboard и перетащите еще один image view в ваш контейнер видов. Измените размер так, чтобы он полностью его. Также проследите, чтобы ваш новый image view располагался в схеме документа сразу под контейнером видов. Назовите новый image view plate_cheese.

И еще кое-что мы должны добавить в нашу корзинку. Каким-то образом, несмотря на все ваши предосторожности, пронырливый жучок все-таки пробрался к нашей еде! Но не волнуйтесь, у нас есть секретное оружие! Добавьте еще один UIImageView, как подвид контейнера вида, расположите его между plate_cheese и fabric_top и назовите его bug. В Size Inspector установите размеры рамки x: 160, y: 185, width: 129, height: 135.

К этому моменту ваш View Controller Scene должен иметь следующий вид на схеме документа:

  • plate_cheese
  • bug
  • fabric_top
  • fabric_bottom
  • door_top
  • door_bottom

Далее, добавим outlet для нашего нового вида вредителя в ViewController.swift. Зажимая CTRL, перетаскиваем его в редактор, как вы делали это с предыдущими outlet'ми. Назовем этот outlet неожиданным именем bug.

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

var isBugDead = false

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

Далее, добавьте следующие методы:

func moveBugLeft() {
  if isBugDead { return }
 
  UIView.animateWithDuration(1.0,
    delay: 2.0,
    options: .CurveEaseInOut | .AllowUserInteraction,
    animations: {
      self.bug.center = CGPoint(x: 75, y: 200)
    },
    completion: { finished in
      println("Bug moved left!")
      self.faceBugRight()
    })
}
 
func faceBugRight() {
  if isBugDead { return }
 
  UIView.animateWithDuration(1.0,
    delay: 0.0,
    options: .CurveEaseInOut | .AllowUserInteraction,
    animations: {
      self.bug.transform = CGAffineTransformMakeRotation(CGFloat(M_PI))
    },
    completion: { finished in
      println("Bug faced right!")
      self.moveBugRight()
    })
}
 
func moveBugRight() {
  if isBugDead { return }
 
  UIView.animateWithDuration(1.0,
    delay: 2.0,
    options: .CurveEaseInOut | .AllowUserInteraction,
    animations: {
      self.bug.center = CGPoint(x: 230, y: 250)
    },
    completion: { finished in
      println("Bug moved right!")
      self.faceBugLeft()
    })
}
 
func faceBugLeft() {
  if isBugDead { return }
 
  UIView.animateWithDuration(1.0,
    delay: 0.0,
    options: .CurveEaseInOut | .AllowUserInteraction,
    animations: {
      self.bug.transform = CGAffineTransformMakeRotation(0.0)
    },
    completion: { finished in
      println("Bug faced left!")
      self.moveBugLeft()
    })
}

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

И вот еще что, обратите внимание на .AllowUserInteraction в опциях параметра. Это нужно для того, чтобы была возможность отвечать на прикосновения (чтобы жука раздавить), в то время, как будет протекать анимация.

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

Вращение жука устанавливается вспомогательной функцией CGAffineTransformMakeRotation, которая создает трансформацию вращения и угловой параметр отображается в радианах. С этой целью мы используем  M_PI для отображения вращения на 180 градусов.

Все что теперь остается, так это запустить анимационную цепочку! Добавьте следующую строку в конец viewDidLoad:

moveBugLeft()

Посмотрите что у вас получилось!

Прищучь жука!

Вот и наступил момент, который мы все так долго ждали!

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

func handleTap(gesture: UITapGestureRecognizer) {
  let tapLocation = gesture.locationInView(bug.superview)
  if bug.layer.presentationLayer().frame.contains(tapLocation) {
    println("Bug tapped!")
    // add bug-squashing code here
  } else {
    println("Bug not tapped!")
  }
}

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

На самом верху класса, сразу после var isBugDead = false, добавьте tap, для распознавания движения:

let tap: UITapGestureRecognizer?

Создайте распознаватель движения в init сразу после super.init. Это должно стоять после super.init, так как один из параметров self и вы не можете использовать self до того, как он был инициализирован.

tap = UITapGestureRecognizer(target: self, action: Selector("handleTap:"))

Далее, добавьте следующие строки в конец viewDidLoad():

view.addGestureRecognizer(tap!)

Этот код инициализирует новое движение и затем добавляет его на вид.

Запустите приложение и нажмите на экран пальцем. В зависимости от того, куда вы нажмете, выведется сообщение "Bug tapped!" или "Bag not tapped!" в консоли Xcode.

Самая удовлетворяющая нас часть будет следующей. Найдите handleTap() и добавьте следующий код внутри блока if, где есть случай нажатия на жука:

if isBugDead { return }
view.removeGestureRecognizer(tap!)
isBugDead = true
UIView.animateWithDuration(0.7, delay: 0.0, options: .CurveEaseOut, animations: {
  self.bug.transform = CGAffineTransformMakeScale(1.25, 0.75)
}, completion: { finished in
  UIView.animateWithDuration(2.0, delay: 2.0, options: nil, animations: {
    self.bug.alpha = 0.0
  }, completion: { finished in
    self.bug.removeFromSuperview()
  })
})

Как только вы нажимаете на жука, ваша первая установка isBugDead становится true, так что анимация перестает исполняться. Потому это движение удаляется из вида, так что больше никаких взаимодействий происходить не может. Потом, можно начать новую цепочку анимаций: сначала жук сплющивается, с помощью трансформации масштаба, затем исчезает, используя прозрачность установленную на 0. После того, как жук исчезает, мы удаляем его из superview.

Запустите, на этом этапе у вас появилась возможность раздавть жука!

Звуковой эффект

Это делать совершенно необязательно, но очень весело!

Сначала перетащите папку Sounds из архива в проект, и убедитесь что вы выбрали, что файлы должны быть скопированы и сгруппированы.

Снова идем в ViewController.swift и вписываем импорт на самую верхушку файла:

import AVFoundation

AVFoundation включает в себя проигрывание музыки, так что нам нужно ее импортировать для максимального эффекта раздавливания.

Потом, добавьте свойство в объявление класса:

let squishPlayer: AVAudioPlayer

Теперь, когда мы создали экземпляр проигрывателя для вашего звукового файла,

добавьте следующий метод инициализатора.

required init(coder aDecoder: NSCoder) {
  let squishPath = NSBundle.mainBundle().pathForResource("squish", ofType: "caf")
  let squishURL = NSURL(fileURLWithPath: squishPath!)
  squishPlayer = AVAudioPlayer(contentsOfURL: squishURL, error: nil)
  squishPlayer.prepareToPlay()
 
  super.init(coder: aDecoder)
}

Этот код проверяет, что экземпляр AVAudioPlayer установлен, когда view controller инициализирован. Если вы знаете Objective-C, то вам может показаться странным, что мы вызываем super.init() в конце инициализатора. В Swift инициализатор должен всегда проверять, чтобы все переменные экземпляра были инициализированы до того, как будет вызван инициализатор суперкласса, что делает безопасным вызов методов экземпляра, и методов суперкласса. Помните, что переменные экземпляра с исходными значениями (isBugDead) не нуждаются в установке, в назначенном инициализаторе.

Наконец, добавьте следующую строку в handleTap(), сразу после isBugDead в true:

squishPlayer.play()

Это позволит сыграть звук сразу, в момент нажатия на жука. Вот и все! Запустите свое приложение, прибавьте громкость и хрустите своим жучком, получая при этом немыслимое удовольствие!

Вот конечный вариант вашего проекта, включающий весь код, написанный в этом туториале.

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

В этом туториале мы использовали только функцию animateWithDuration(duration, delay, options, animations, completion), однако есть еще несколько функций, которые мы бы могли использовать для анимации:

  • animateWithDuration(duration, animations)
  • animateWithDuration(duration, animations, completion)
  • animateKeyframesWithDuration(duration, delay, options, animations, completion)
  • performSystemAnimation(animation, onViews, options, animations, completion)
  • animateWithDuration(duration, delay, usingSpringWithDamping, initialSpringVelocity, options, animations, completion)

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

Что дальше?

Дальше, вы можете продолжить изучать наши туториалы по мере их появления, а также, параллельно читать перевод официальной книги по языку программирования Swift. И, для более подробного изучения языка, вы можете пройти наши курсы!

Урок подготовил: Иван Акулов

Источник урока: http://www.raywenderlich.com/76200/basic-uiview-animation-swift-tutorial