UIKit динамика в Swift

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

Мало сделать цифровой интерфейс, который выглядит реально, нужно сделать так, чтобы он ощущался как реальный. И для этого у вас есть несколько новых модных инструментов: UIKit Dynamics и Motion Effects.

  • UIKit Dynamcs - это полный физический двигатель, интегрированный в UIKit. Он позволяет вам создавать интерфейсы, которые выглядят реальными, при помощи добавления поведения (режимов), например гравитации, или действия других сил. Вы можете определять любые физические черты, какие пожелаете, связанные с поддержкой вашего интерфейса, а об остальном позаботится динамический двигатель.
  • Motion Effects (или эффекты движения) позволяют вам создавать параллакс эффекты, аналогичные тем, что мы уже видели на домашнем экране iOS 7 и iOS 8. Вообщем, вы можете использовать данные, предоставленные акселерометром телефона для того, чтобы создавать интерфейсы, которые реагируют на изменения в ориентации телефона.

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

Поехали!

Работа с UIKit Dynamics может доставить вам кучу удовольствия!

Откройте Xcode выберите File/New/Project..., затем выберите iOS Application / Single View Application и назовите его как DynamicsDemo. После того, как проект создан, откройте ViewController.swift и добавьте несколько строчек в viewDidLoad:

let square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
square.backgroundColor = UIColor.grayColor()
view.addSubview(square)

Код выше добавляет квадратик UIView в ваш интерфейс.

Постройте и запустите свое приложение и вы увидите одинокий серый квадратик, грустно торчащий на экране:

Если вы запускаете ваше приложение на каком-то устройстве, попробуйте наклонить его, перевернуть сверху-вниз. Что произошло? Ничего? И это правильно, все работает именно так, как и должно. Когда вы добавляете вид (view) на ваш интерфейс, то вы ожидаете, что он будет располагаться там, где где он был определен, до тех пор, пока к нему не применится какая-либо динамика.

Добавление гравитации

Все еще продолжаем работать с ViewController.swift, добавьте код над viewDidLoad:

var animator: UIDynamicAnimator!
var gravity: UIGravityBehavior!

Эти свойства - неявно извлеченные опционалы(определяется по !, после имени типа). Эти свойства должны быть опциональными, потому что вы не хотите инициализировать в методе init вашего класса. Вы можете использовать неявно-извлеченные опционалы, потому что мы знаем, что эти свойства не будут равны nil, после того как мы их инициализировали. Это ограждает вас от ручного извлечения их значения при помощи !.

Добавьте следующий код в viewDidLoad:

animator = UIDynamicAnimator(referenceView: view)
gravity = UIGravityBehavior(items: [square])
animator.addBehavior(gravity)

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

В коде, который вы только что добавили, есть пара классов динамики с которыми можно поиграть:

  • UIDynamicAnimator - физический двигатель UIKit. Этот класс отслеживает поведение, которое вы добавляете в двигатель. Когда вы создаете экземпляр аниматора, вы передаете тот referenceView, который использует аниматор для определения своей системы координат.
  • UIGravityBehavior моделирует режим гравитации, оказывает влияние сил на один или несколько элементов, что позволяет моделировать физические взаимодействия. Когда вы создаете экземпляр поведения, вы связываете его с несколькими значениями, обычно с видами (Views). Так вы можете выбрать на какие предметы распространяется поведение, в нашем случае, на какие элементы у нас действует сила гравитации.

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

Заметка

Немого о единицах измерения: в физическом мире гравитация (g) имеет размерность метры в секунду в квадрате, что равно 9,81 м\с2. Используя Ньютоновский второй закон, вы можете посчитать как далеко должен упасть объект под действием силы гравитации по такой формуле:

distance = 0.5 * g * time2

В UIKit Dynamics формула такая же, но размерность другая. Вместо метров в квадрат секунды, используются тысячи пикселей в квадрат секунды. Используя второй закон Ньютона, вы все еще можете точно сказать, где будет располагаться ваш объект в конкретное время, в зависимости от компонентов гравитации, которые вы будете использовать.

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

Устновка границ

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

Добавьте следующее свойство в ViewController.swift:

var collision: UICollisionBehavior!

Добавьте эти строки в конец viewDidLoad:

collision = UICollisionBehavior(items: [square])
collision.translatesReferenceBoundsIntoBoundary = true
animator.addBehavior(collision)

Пример выше создает поведение столкновения (collision), которое определяет одну или несколько границ, с которыми взаимодействует объект.

Вместо того, чтобы явно добавлять координаты границ в коде выше, мы устанавливаем значение свойства translatesReferenceBoundsIntoBoundary как true. Это заставляет границу использовать ограничения самого контейнера квадрата в UIDynamicAnimator.

Стройте и запускайте. Вы увидите, как квадрат ударяется о нижний край экрана, немного подпрыгивает и перестает двигаться:

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

Обработка столкновений

Следующее что мы добавим - это недвижимый барьер, о который будет ударяться падающий квадрат.

Вставьте следующий код в viewDidLoad сразу после строк, где вы добавляете квадрат в уже существующий view:

let barrier = UIView(frame: CGRect(x: 0, y: 300, width: 130, height: 20))
barrier.backgroundColor = UIColor.redColor()
view.addSubview(barrier)

Запустите приложение. Вы увидите красный барьер, который располагается по середине экрана. Однако этот барьер малоэффективен, так как наш квадрат пролетает сквозь него:

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

Посмотрим схему:

UIDynamicAnimator связан с главным видом (reference view), который предоставляет нам координаты системы. Затем, вы добавляете одно или более поведений, которые используют силы для воздействия на объекты, связанные с ними. Большинство поведений могут взаимодействовать с несколькими объектами, и каждый объект может быть связан сразу с несколькими поведениями. Диаграмма выше показывает текущие режимы поведения, и их связи внутри приложения.

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

Столкновения объектов

Чтобы сделать так, чтобы этот квадрат взаимодействовал с барьером, найдите инициализацию поведения столкновения и замените вот такой строкой:

collision = UICollisionBehavior(items: [square, barrier])

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

Запустите приложение и у вас должно получиться что-то вроде этого:

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

Обновим нашу диаграмму так, чтобы вы могли видеть, что наше столкновение сейчас связано с обоими видами:

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

Выглядит так, будто нам нужно что-то пересмотреть. Так как вид барьера должен быть неподвижен, то это значит, что мы не должны применять к нему какую-либо динамику. Но как же тогда осуществить столкновение?

Невидимые границы и столкновения

Давайте поменяем нашу инициализацию поведения столкновения назад, к ее начальному виду, чтобы в ней был прописан только квадрат:

collision = UICollisionBehavior(items: [square])

Сразу после этой строки добавьте:

// add a boundary that has the same frame as the barrier
collision.addBoundaryWithIdentifier("barrier", forPath: UIBezierPath(rect: barrier.frame))

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

Запустите ваше приложение и посмотрите на него в действии:

Теперь, квадрат ударяется об ограничение, немного прокручивается и продолжает свое путешествие на "дно" экрана, где и принимает положение покоя.

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

Обратная сторона столкновений

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

collision.action = {
  println("\(NSStringFromCGAffineTransform(square.transform)) \(NSStringFromCGPoint(square.center))")
}

Код выше записывает лог центра и свойств трансформации для падающего квадрата. Запустите ваше приложение и вы увидите сообщение в консоли Xcode.

Для первых ~400 миллисекунд вы должны увидеть вот такое сообщение:

[1, 0, 0, 1, 0, 0], {150, 236}
[1, 0, 0, 1, 0, 0], {150, 243}
[1, 0, 0, 1, 0, 0], {150, 250}

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

Как только квадрат ударяется о барьер, он начинает вращаться, что отображается в логе как:

[0.99797821, 0.063557133, -0.063557133, 0.99797821, 0, 0] {152, 247}
[0.99192101, 0.12685727, -0.12685727, 0.99192101, 0, 0] {154, 244}
[0.97873402, 0.20513339, -0.20513339, 0.97873402, 0, 0] {157, 241}

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

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

Для подписания метода, для динамического поведения, используют термин "предмет". Единственное требование для использования динамического поведения на объекте - это соответствие протоколу UIDynamicItem:

protocol UIDynamicItem : NSObjectProtocol {
  var center: CGPoint { get set }
  var bounds: CGRect { get }
  var transform: CGAffineTransform { get set }
}

Протокол UIDynamicItem дает динамике доступ для чтения и редактирования свойств позиционирования центра, позволяя двигать предметы, основываясь на его внутренних вычислениях. Он так же имеет доступ к границам формата "только для чтения", который он использует для определения размеров предмета. Это позволяет создавать границу соударения по периметру всего предмета, а так же позволяет высчитывать массу, при применении свойств.

Этот протокол означает, что не только динамика тесно связана с UIView, но на самом деле есть и другой класс UIKit, который не является видом, но все еще соответствует этому протоколу: UICollectionViewLayoutAttributes. Это позволяет динамике анимировать предметы в наборах видов.

Уведомления о соударении

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

В ViewController.swift пропишем протокол UICollisionBehaviorDelegate, вписав в объявление класса:

class ViewController: UIViewController, UICollisionBehaviorDelegate {

В viewDidLoad, установите view controller в качестве делегата, сразу после после инициализации соударения объектов:

collision.collisionDelegate = self

Следующим шагом, добавьте реализацию для одного из режимов соударения методов делегата в классе:

func collisionBehavior(behavior: UICollisionBehavior!, beganContactForItem item: UIDynamicItem!, withBoundaryIdentifier identifier: NSCopying!, atPoint p: CGPoint) {
  println("Boundary contact occurred - \(identifier)")
}

Этот метод вызывается когда происходит соударение - выводит лог в консоли. Для избежания загромождения логов в вашей консоли, просто удалите collision.action, который вы добавили в предыдущей секции.

Запустите приложение. Ваши объекты будут взаимодействовать и вы увидите следующие сообщения в консоли:

Boundary contact occurred - barrier
Boundary contact occurred - barrier
Boundary contact occurred - nil
Boundary contact occurred - nil
Boundary contact occurred - nil
Boundary contact occurred - nil

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

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

Под строкой, которая отправляет сообщения в лог, добавьте:

let collidingView = item as UIView
collidingView.backgroundColor = UIColor.yellowColor()
UIView.animateWithDuration(0.3) {
  collidingView.backgroundColor = UIColor.grayColor()
}

Строим, запускаем и смотрим на полученный эффект:

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

Таким образом, UIKit Dynamics автоматически установила физические свойства наших предметов (масса, упругость), основываясь на вычислениях границ предметов. Далее вы увидите, как можно контролировать эти физические свойства самостоятельно, используя класс UIDynamicItemBehavior.

Конфигурация свойств предметов

Внутри viewDidLoad добавьте следующий код в самый конец метода:

let itemBehaviour = UIDynamicItemBehavior(items: [square])
itemBehaviour.elasticity = 0.6
animator.addBehavior(itemBehaviour)

Код выше создает вариант поведения (режим) itemBehaviour, связанного с этим квадратом, и передает его как объект поведения аниматора. Свойство эластичности контролирует подпрыгивания предмета, а значение 1.0 свидетельствует о полной эластичности, то есть энергия при соударении не теряется. Вы установили пластичность квадрата на 0.6, что значит, что он будет терять часть энергии при каждом подпрыгивании.

Запустите ваше приложение и вы заметите, что квадрат скачет:

Заметка

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

var updateCount = 0
collision.action = {
  if (updateCount % 3 == 0) {
    let outline = UIView(frame: square.bounds)
    outline.transform = square.transform
    outline.center = square.center
 
    outline.alpha = 0.5
    outline.backgroundColor = UIColor.clearColor()
    outline.layer.borderColor = square.layer.presentationLayer().backgroundColor
    outline.layer.borderWidth = 1.0
    self.view.addSubview(outline)
  }
 
    ++updateCount
}

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

  • Эластичность (elacity) - определяет насколько эластичным будет столкновение, или, другими словами, насколько "резиновым" будет столкновение.
  • Трение (friction) - определяет сопротивление движению, когда идет скольжение по поверхности.
  • Плотность (density) - когда объединяется с размером, то дает нам массу предмета. Чем тяжелее тело, тем тяжелее его ускорить или затормозить.
  • Сопротивление (resistance) - определяет сопротивление для любого линейного движения, в отличии от трения, которое применимо только при движении скольжением.
  • Угловое сопротивление (angularResistance) - определяет сопротивление вращению.
  • Допуск вращения (allowsRotation) - это интересный параметр, который не моделирует ничего из физики реального мира. Если вы поставите значение как NO, то объект не будет вращаться, не смотря на все крутящие моменты, которые воздействуют на предмет.

Добавление вариантов поведения динамически

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

Откройте ViewController.swift и добавьте следующее свойство над viewDidLoad:

var firstContact = false

Теперь добавьте следующий код в конец метода делегата столкновения collisionBehavior(behavior:beganContactForItem:withBoundaryIdentifier:atPoint:):

if (!firstContact) {
  firstContact = true
 
  let square = UIView(frame: CGRect(x: 30, y: 0, width: 100, height: 100))
  square.backgroundColor = UIColor.grayColor()
  view.addSubview(square)
 
  collision.addItem(square)
  gravity.addItem(square)
 
  let attach = UIAttachmentBehavior(item: collidingView, attachedToItem:square)
  animator.addBehavior(attach)
}

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

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

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

Взаимодействие с пользователем

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

Удалите код, который вы добавили в последней секции: и свойства firstContact, и конструкцию if в collisionBehavior(). Эффект UISnapBehavior проще увидеть тогда, когда на экране имеется всего один квадрат.

Добавьте два свойства:

var square: UIView!
var snap: UISnapBehavior!

Теперь, у вас есть наблюдение за вашим видом квадрата, так что вы можете получить к нему доступ с любого места вашего view controller'а. Следующим вы будете использовать объект snap.

Во viewDidLoad уберите ключевое слово let от объявления квадрата, так что он использует новое свойство, вместо локальной переменной:

square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))

Наконец, добавьте реализацию для touchesEnded, для создания и добавления нового поведения нажатия, когда пользователь будет касаться экрана:

override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
  if (snap != nil) {
    animator.removeBehavior(snap)
  }
 
  let touch = touches.anyObject() as UITouch 
  snap = UISnapBehavior(item: square, snapToPoint: touch.locationInView(view))
  animator.addBehavior(snap)
}

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

Запустите ваше приложение. Попробуйте потыкать вокруг: квадрат должен перемещаться везде, где бы вы не нажали!

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

Что дальше?

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

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

Источник урока: http://www.raywenderlich.com/76147/uikit-dynamics-tutorial-swift