Задание 4 Stanford CS 193P Fall 2017. Анимационная игра Set. Решение обязательных пунктов 1-6.

Содержание

В этом Задании вы добавите анимацию в вашу игру Set и скомбинируете ваши 3 первых Задания в одно.
Текст Домашнего задания на английском языке доступен на  iTunes в пункте “Programming: Project 4: Animayed Set″. На русском языке вы можете скачать Задание 4 здесь:

Задание 4 Игра Графический Set iOS 11.pdf

 Вам необходима реализация Заданий 1- 3. Начинаем выполнять с кода Задания 3.

Для решения Задания 4 необходимо ознакомиться с Лекциями 7 — 10.

Мое решение Задания 4 состоит из двух приложений: анимационной игры Set БЕЗ использования делегата UIDinamicAnimationDelegate динамического аниматора, которая находится в папке Set IV NoExtra, и  анимационной игры Set с использованием делегата UIDinamicAnimationDelegate, которая находится в папке Set IV NoExtra Stasis.

Все это находится на Github для iOS 11 и на Github для iOS 12.

Пункты 1, 2 и 3 обязательные

1. Ваше приложение должно продолжать играть в соло версию игры Set, как это требовалось в Задании 3 (с особенностями, указанными ниже).

2. Вы должны анимировать следующие действия в игре Set:

  • Реорганизация карт. Если карты добавляются или исчезают из игры, то карты должны перемещаться плавно ( не прыгать мгновенно) на их новые позиции.
  • Сдача новых карт. Это включает как сдачу начальных 12 карт, так и сдачу 3-х новых карт в любое время. Карты должны “лететь” через экран из некоторой “колоды”, расположенной где-то на экране. Внешний вид колоды полностью отдается на ваше усмотрение. Никакие две карты не должны сдаваться одновременно, хотя их анимации могут немного перекрываться.
  • Обнаружение совпадения. Все «совпавшие» карты должны “улетать”  с тех мест, где они находились одновременно и начать отскакивать от краев экрана в течение пары секунд прежде, чем собраться в некоторую “стопку сбрасывания”, где-то расположенную на экране. Внешний вид “стопки сбрасывания” полностью отдается на ваше усмотрение.
  • Переворот карт. Карты сдаются “лицевой” стороной вниз до тех пор, пока не достигнут своего местоположения, затем они должны быть перевернуты для проявления содержимого карты игры Set. Для совпавших» карт после того, как они «улетели» в  “стопку сбрасывания”, по крайней мере верхняя карта в этой “стопке сбрасывания” должна лежать “лицевой” стороной вниз.

3. Ваша реализация анимации должна использовать UIViewPropertyAnimator, UIDynamicAnimator и class метод transition(with:…) класса UIView. Возможно, вам понадобиться таймер Timer, но это не строго обязательно.

Начинаем наш проект с модификации проекта Задания 3, который находится на GitHub

Нам нужно анимировать Реорганизацию карт.

Наши карты располагаются и рассчитывают свои frames в области boardViewОбласть boardView обслуживается класссом BoardView, в котором основной public переменной var является массив cardViews, представляющий собой множество Set карт, расположенных на экране (а точнее в области boardView):

Карты cardViews располагаются на экране с помощью вспомогательного объекта «сетка» Grid, это структура struct, позволяющая вычислять frame любой из карт cardViews в зависимости от границ bounds области boardView, количества карт cardViews.count и соотношения сторон AspectRatio Set карты. Мы будем анимировать изменение местоположения карт cardViews с помощью аниматора свойств UIViewPropertyAnimator в методе layoutSetCards ():

Мы видим, что добавление Set карт на игровое поле BoardView происходит последовательно строками (одна за другой), так как время задержки анимации delay пропорционально номеру строки row. И общее время анимации «Реорганизации карт» будет зависеть от количества строк rowsGrid. Именно поэтому переменная rowsGrid сделана public для этого класса, она может нам еще понадобиться.

Где мы должны разместить этот код, зависящий от границ bounds?

Для этого существует метод layoutSubviews ():

Мы видим, что наша «сетка» gridCards реагирует не только на изменение границ bounds, но и на изменение количества карт cardViews.count, но и на соотношение сторон Constant.cellAspectRatio. Поэтому мы должны вызывать НЕЯВНО метод  layoutSubviews () при добавлении или удаление карт из игрового поля или при полном удалении карт из игрового поля:

Для НЕЯВНОГО вызова метода  layoutSubviews () мы использовали вызов метода layoutIfNeeded(), так как в подсказке № 2 сказано:

Если вы хотите, чтобыlayoutSubviews() был вызван немедленно ( а не “когда-то в будущем, когда это будет удобно”), то вы не вызываете layoutSubviews(), вы вызываете layoutIfNeeded(). Заметьте, что это реально вызовет layoutSubviews() для нового размещения subviews, если с прошлого вызова layoutSubviews(), a) изменились границы bounds этого view, b) некоторые subviews были добавлены или удалены, или  c) кто-то вызвал setNeedsLayout(). Так что чувствуйте себя свободно при вызове layoutIfNeeded(), если вы хотите, чтобы метод layoutSubviews() для вашего view был вызван, но вы также обязательно вызывайте setNeedsLayout() каждый раз, когда вы изменяете что-то в вашем view, что опять заставит его заново разместить его subviews.

Вот как плавно меняется местоположение карт (строка за строкой) в зависимости от количества карт на игровом столе:

Или в зависимости от границ bounds:

Теперь нам нужно анимировать «Сдачу новых карт«.

Как нам советуют в подсказке № 1, взглянем на метод updateViewFromModel():

Точнее нам придется иметь дело с методом updateCardViewsFromModel() обновления карт на игровом поле boardView:

Логика метода updateCardViewsFromModel()  очень простая. Мы все время сопоставляем количество карт на игровом столе в нашей Модели —game.cardsOnTable — и действительное количество карт в  игровом поле — boardView.cardView.

Если на игровом поле карт больше, чем в Модели, а это происходит тогда, когда  оставшиеся на игровом поле «совпавшие» карты matchedSetCardViews нечем заменить в конце игры, так как в колоде карт не осталось.

Удаление карт matchedSetCardViews с boardView вызовет анимацию «Реорганизации карт«, но это будет происходить очень редко в основном в конце игры.

Если на игровом поле карт не хватает, например, в самом начале игры или когда в Модель добавлены 3 дополнительных карты (либо при нажатии кнопки пользователем, либо автоматически при  замене «совпавших» карт на новые), то мы создаем новые карты cardView в игровом поле), накапливая их в массиве newCardViews, а затем их добавляем на игровое поле:

Добавление карт newCardViews на boardView также вызовет анимацию «Реорганизации карт«, но это не то, что нам бы хотелось. Нам нужно реализовать анимацию «Сдача новых карт«, когда карты должны “лететь” через весь экран из некоторой “колоды”, расположенной где-то на экране и никакие две карты не должны сдаваться одновременно, хотя их анимации могут немного перекрываться. Для того, чтобы это сделать воспользуемся некоторой хитростью, описанной в подсказке № 3h:

Все карты, которые необходимо “сдать” (deal) уже находятся в данный момент на правильных позициях (но с alpha = 0, так что пользователь их не видит). Так как они невидимы, вы можете поместить их в колоду “прыжком” (мгновенно). Как только они там оказались, установите (нет необходимости в анимации) их alpha = 1 для того, чтобы они там появились заново (надеюсь, ваша колода нарисована таким образом, что это не будет выглядеть так, что эта карта просто появляется из ниоткуда). Теперь вы должны анимировать их frames назад, туда, откуда они стартовали (один за другим).

Если у вашей колоды другой размер, чем у карты, которую вы “сдаете”, чувствуйте себя свободно и изменяйте размер карты, когда вы перемещаете ее в колоду (пока ее alpha =0, так что пользователь ее не видит). Так как вы анимируете ее на прежнее место, используя ее frame, то она будет анимировать увеличение или сжатие до правильных размеров автоматически.

Именно поэтому все новые карты newCardViews добавляются в игровое поле boardView невидимыми, хотя там они размещаются на нужных местах и становятся нужного размера с помощью анимации «Реорганизации карт«, но для новых карт мы ничего этого не видим. Для них мы запускаем анимацию «Сдачи карт» с помощью функции dealAnimation() :

На самом деле мы определяем карты, подлежащие анимации «Сдача карт«, dealSetCardViews как карты boardView.cardView, у которых alpha =0:

Сама анимация «Cдача одной карты» SetCardView выполняется с помощью метода animateDeal (from deckCenter: CGPoint, delay: TimeInterval), который анимирует перемещение карты ТИПА SetCardView из точки deckCenter в свое первоначальное месторасположения, а затем переворачивает ее «лицевой» стороной вверх, попутно меняя свой размер bounds от 60% своего размера до 100%, этот метод находится в классе SetCardView:

Сначала с помощью UIViewPropertyAnimator анимируются свойства center и bounds карты  SetCardView и происходит «полет» карты через весь экран с небольшим увеличением размера, а затем с помощью метода transition(with:…) класса UIView анимируется свойство isFaceUp и происходит переворот карты «лицевой» стороной вверх.

 Если мы внимательно посмотрим на метод  dealAnimation(), то заметим там присутствие таймера Timer, который обрамляет всю нашу анимацию «Сдача карт«:

Причем таймер Timer срабатывает однократно с задержкой timeInterval, который зависит от числа строк boardView.rowsGrid в сетке расположения Set карт в игровом поле boardView. Это связано с подсказкой № 3d:

Вам не следует начинать анимацию “Сдачи карт” до тех пор, пока  анимация “Реорганизации карт” не завершилась.

Это очень важный момент, потому что иначе анимация «Сдача карт» начинается не в той точке (особенно когда число строк становится большим), но мне не удалось придумать, как точно фиксировать окончание анимации «Сдача карт«, поэтому я использовала приблизительное оценочной время задержки timeInterval, вычисленное на основе числа строк boardView.rowsGrid, и это хорошо работает. 

Теперь поговорим о точки, откуда происходит анимация «Сдача карт«. Я хотела бы, чтобы это была середина стека Stack View, в котором по центру находится «Колода карт»:

Для начала сделаем Outlet с именем stackMessage на наш стек:

Если мы хотим использовать центр стека stackMessage.center, то мы получим его в системе координат топового View (которое self.view), а нам нужна эта точка в системе координат boardView, так как для всех наших карт superview — это boardView, поэтому используем преобразование convert(, to/from:) в классе UIView из одной системы координат в другую:

В подсказке № 12 нам советуют проверить boardView, так как карты cardsView, для которых он является superview, могут могут выходить за его границы:

В Interface Builder в Инспекторе Атрибутов есть опция Clip To Bounds для view, которая означает, будет ли этот view “обрезать” (clip) свои subviews по своим границам bounds или нет. Убедитесь, что вы установили правильно эту опцию, если вы пытаетесь рисовать subview за пределами границ bounds вашего superview (даже если это происходит в процессе анимации).

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

Вызываем функцию dealAnimation() в методе updateCardViewsFromModel() сразу после добавления новых карт в игровое поле:

Вот как выглядит анимация «Сдача карт» на начальной стадии игры:

Рассмотрим анимацию «Обнаружение совпадения», которая выполняется с помощью метода flyAwayAnimation, который вызывается в  updateCardViewsFromModel(), сразу после добавления новых карт :

Мы знаем, что все «совпавшие» карты должны “улетать”  с тех мест, где они находились одновременно и начать отскакивать от краев экрана в течение пары секунд прежде, чем собраться в некоторую “стопку сбрасывания”, где-то расположенную на экране.

Первое, что нам необходимо сделать в методе flyAwayAnimation(), это определить факт совпадения 3-х карт, и это делается с помощью переменной isSet  игры game:

Кроме того, мы должны застраховаться от повторного «улетания карт«, если мы будем использовать метод updateCardViewsFromModel() в наших дальнейших манипуляциях, а мы определенно будем это делать при подсказках о присутствии Sets в множестве расположенных на экране карт. Поэтому после «улетания карт» мы пометим «совпавшие» карты matchedSetCardViews с помощью alpha, значение которого расположено в диапазоне от 0 до 1 и не равно ни 1, ни 0. И наша анимация «улетания карт» будет работать только со свежими ( с alpha = 1) «совпавшими» картами.

При проектировании анимации «улетания карт» надо принять во внимание следующие подсказки:

3g.  Помните, что анимации “улетания карт” (“flyaway”) и “сдачи карт” ( “deal”) вызваны различными действиями пользователя. “Улетание карт” (“flyaway”) происходит, когда нажимается ПОСЛЕДНЯЯ из 3-х “совпадающих” карт.  Анимация “сдачи карт” (“deal”) происходит при нажатии кнопки “сдача 3-х новых карт”  или когда выбрана СЛЕДУЮЩАЯ карта ПОСЛЕ выбора той карты,  которая вызвала 3-х карточное совпадение.

3h.  Анимация “улетание карт”  — это совершенно другая анимация, отличающаяся от анимации “сдачи” карт, так что времена их запуска никак не должны координироваться. Фактически, если вы в итоге реализуете автоматическую “сдачу” 3-х новых карт, то они могут начать свой полет в то же самое время, когда “улетающие” карты “отскакивают” друг от друга и от границ экрана. Круто.

3l.  Это также означает, что UIViews, которые используются в анимация “улетания карт”  должны отличаться от тех UIViews, которые участвовали в “сдаче” карт. Возможно, лучше всего создать временные карты для “улетание карт”, оставив оригинальные карты на месте с alpha = 0 (что вызовет анимацию “сдачи” для замены их на новые на следующем шаге взаимодействия пользователя!).

Так что, если мы решили выполнять анимацию «улетания карт» над «совпавшими» картами matchedSetCardViews, то мы должны создать временные карты tmpCards, которые будут копиями «совпавших» карт matchedSetCardViews и которые будут подвергаться анимации «улетания карт», а  «совпавшими» картами matchedSetCardViews мы оставим на месте, сделав их слабо видимыми с alpha = 0.2, которые на следующем шаге игры будут либо заменены на новые или удалены с игрового поля, если в колоде не осталось карт. Но все это логика игры Set, которая заложена в классе SetGame, и которую мы не изменяли с Задания 1:

Затем добавляем сформированные временные карты tmpCards на игровое поле boardView и к «поведению» cardBehavior, о котором мы поговорим чуть позже и которое обеспечивает карте первоначальный «толчок» (push), а затем столкновение между собой и с границами экрана, позволяя им вращаться:

Через пару секунд мы останавливем столкновение карт и заставляем их собраться в некоторую “стопку сбрасывания”, в точку discardPileCenter, находящуюся в середине кнопки setButton, имитирующей “стопку сбрасывания”:

Точку discardPileCenter, расположенную в середине кнопки setButton, которая в свою очередь находится в стеке stackMessage опять рассчитываем с помощью преобразование convert(, to/from:) в классе UIView из одной системы координат в другую :

Метод animateFly анимации «полета карты в стопку «сброса»» принадлежит визуальному представлению Set карты, то есть классу SetCardView:

У этого метода есть внешнее замыкание addDiscardPile: (() -> Void)?, которое можно выполнить по завершении этих двух анимаций, и мы используем его для того, чтобы сначала (для первой «улетающей» карты) убрать кнопку setsButton, чтобы она не мешала видеть, как «улетевшие» карты складываются в «стопку сбрасывания», а затем добавить ее, но уже с новым значением числа выигранных Sets. Для этого мы добавляем замыкание addDiscardPile к первой временной карте tmpCard[0] и к последней временной карте tmpCard[1], но с разными по содержанию замыканиями:

После «сдачи» карт мы воспользовались подсказкой (кнопкой слева, на которой написано «1 sets«) и выберем этот Set. Далее наши карты «полетели» и начали сталкиваться друг с другом и с краями экрана:

затем они собираются в «стопку сбрасывания»:

Выполняем анимацию “улетание карт” с помощью динамического аниматора UIDynamicAnimator и «поведения» cardBehavior:

Динамический аниматор animator действует на всем экране, а точнее Поведение «улетание карты» собрано в subclass  CardBehavior класса UIDynamicBehavior. В нем представлено 3 «поведения»:

  • «поведение столкновения» collisionBehavior:

  • мета- «поведение» itemBehavior, в котором мы задаем упругость для отскока:

  • «поведение мгновенного толчка» push, который имеет случайные направление push.angle и силу push.magnitude, а также сразу же удаляется из динамического аниматора после завершения толчка: 

Я добавляю в мой класс CardBehavior функцию func addItem( _  item: UIDynamicItem), которая добавляет  динамический элемент item к моему “поведению” CardBehavior:

Внутри этой функции  item добавляется к моим “дочерним” “поведениям”: к “поведению” collisionBehavior, к “поведению” itemBehavior, и я “толкаю” мой динамический объект item с помощью функции push, которая по существу добавляем ему “поведение” “толчка” push.

Что касается замыкания push.action, то здесь мы убираем “поведение” push сразу же, как только оно завершилось, и НЕ напрямую из dynamicAnimator. Вместо этого я сделала “поведение”  push “дочерним” “поведением” моего “поведения” CardBehavior, а затем убрала его как “дочернее” “ поведение”:

Внутри замыкания необходимо поставить self., но в результате у нас образовалась “циклическая ссылка памяти”, потому что  динамическое “поведением” CardBehavior определенно имеет указатель на замыкание action, так как оно указывает на свое “дочернее” “поведение”, одним из которых является push “поведение”. И “поведение” push теперь указывает на него. Я должна избавиться от этой “циклической ссылки памяти”, сделав  self внутри замыкания weak self :

Итак, мы разорвали эту “циклическую ссылку памяти”. Но мне нужен также unowned push, потому что я передаю его в качестве аргумента функции removeChildBehavior внутри замыкания action.

Но я не хочу делать unowned self в нашем случае, потому что, если по какой-то причине целое наше “поведение” cardBehavior будет убрано из “кучи” (heap), то мое приложение закончится аварийно, а я этого не хочу. Гораздо более безопасно иметь weak self. C self НЕ та же самая ситуация, как с push, когда я точно знаю, что этот push будет находиться в “куче” (heap) лишь до тех пор, пока action находится в “куче”. Такая ситуация не действительна с self.

Мы добавили метод addItem в наш класс CardBehavior, и точно также нам нужен метод removeItem. Метод removeItem точно такой же, как и addItem, но только внутри этого метода мы УДАЛЯЕМ динамический элемент item из всех “поведений”:

Итак, мы собрали весь код, относящийся к динамической анимации Set карты в класс CardBehavior.  Я хочу добавить еще одну вещь в этот класс CardBehavior — это “удобный” инициализатор convenient init. Это инициализатор позволит мне определить аниматор animator, в котором я буду находиться. Когда вы создаете “удобный” инициализатор convenient init, то все, что вам необходимо сделать, это в самом начале вызвать свой собственный инициализатор self.init, затем вы можете делать что хотите. В нашем случае мы просто просим аниматор animator добавить к нему наше поведение self:

Это значительно упрощает код создания динамического аниматора animator и «поведения» cardBehavior в нашем Controller SetGameViewController:

Фактически, у нас всего две строки кода, которые нам необходимы для динамической анимации карт. Мы весь необходимый для динамической анимации код перенесли в класс CardBehavior. И мы можем очень просто в любой момент добавить временную Set карту tmpView к динамическому «поведению» cardBehavior в методе flyAwayAnimation () :

И в любой момент удалить ее из динамического поведения cardBehavior:

Итак, наши карты прекрасно «улетают» и складываются в «стопку сбрасывания». Нам понадобилось для этого 3 типа анимации:

  1. анимация «Сдача карт«, реализованная методом animateDeal(from deckCenter: CGPoint, delay: TimeInterval) в классе SetCardView
  2. анимация «Улетания карт«, реализованная с помощью динамического аниматора animator: UIDynamicAnimator и динамического «поведения» карты CardBehavior
  3. анимация «Сбор совпавших карт» в «стопке сбрасывания» и их переворот, реализованная методом animateFly(to discardPileCenter: CGPoint, delay: TimeInterval) в классе SetCardView

Позже я расскажу о более сложной динамической анимации, которая будет выполнять не только «улетание карт», но и «сбор совпавших карт» в «стопке сбрасывания» с использованием делегата  UIDynamicAnimatorDelegate, и только переворот карт для этого случая останется за  transition(with:…) класса UIView.

Ну а пока пользователь очутился перед экраном, с которого визуально «улетели» «совпавшие» карты и остались лишь сильно затененные пустые места вместо них. Что пользователь может делать в этой ситуации? В зависимости от обстоятельств у него есть две возможности: на экране остались другие Sets и на экране не осталось никаких больше Sets:

В первом случае вы можете постараться выбрать существующий Set или воспользоваться кнопкой подсказки, на которой написано «2 sets» (оставшиеся на  экране Sets подсвечиваются розовым цветом), а во втором случае кликнуть на любой карте, чтоприведет к «сдачи новых карт» и улучшит ситуацию с Sets. В обоих случаях как только вы кликните на первой же СЛЕДУЮЩЕЙ карте, «за кулисами», то есть в Модели, произойдет замена «совпавших» карт новыми 3-мя картами из колоды. Они уже на нужных местах, но они пока не отображаются на экране, их места по-прежнему на экране заполняют старые «затененные» карты c alpha < 1, и нам необходимо устроить анимацию «их сдачи». Для этого мы включим их в число карт, подготовленных для «сдачи», присвоив им alpha = 0 :

Действительно, когда вы кликнули на СЛЕДУЮЩЕЙ карте после «совпадения 3-х карт», на экране больше не будет 3-х выделенных карт («совпадающих» или нет), так что переменная game.isSet будет равна nil, а карты с alpha отличным от 0 остались. Вот для них и произойдет «сдача» новых карт:

В результате у нас появились новые 3 карты, которые заменили «совпавшие» и, соответственно появились два новых Sets (это видно на кнопке «2 Sets«):

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

Когда вы кликаете на этой кнопке, то никаких анимаций не происходит, только на короткое время подсвечивается РОЗОВЫМ ЦВЕТОМ очередной находящийся на экране Set.

Но стоит нам начать кликать на карты. составляющие Set, как сразу же начнутся анимации. После 1-го клика начнется «сдача» новых карт из колоды на замену старых «совпавших» карт. После 3-го клика, если мы выбрали Set, начнется анимация «улетания карт», и обе эти анимации происходят одновременно:

«Сдача» новых карт заканчивается, вновь «совпавшие» карты «летают» по экрану и сталкиваются, а затем собираются в  «стопку сбрасывания» (справа) и количество выигранных пользователем Sets меняется на 4:

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

Анимация «Улетания карт» с использованием делегата UIDynamicAnimatorDelegate динамической анимации UIDynamicAnimator

Если мы посмотрим на подсказки к Заданию 4, то создается впечатление, что профессор хочет, чтобы анимация «улетания карт» выполнялась полностью динамическим аниматором UIDynamicAnimator, за исключением переворачивания верхней карты «лицевой» стороной вниз. То есть  анимация «улетания карт» как бы состоит из 3-х частей. Первая часть — это «толчок» карты и сталкивание карт между собой и с границами («поведения» pushcollisionBehavior и itemBehavior). Вторая часть — «сбор» карт в «стопке сбрасывания» («поведения» snap). Третья часть — переворачивание хотя бы верхней карты «лицом» вниз. Все части у нас есть, нам нужно только заменить вторую часть на «поведение» «мгновенного снимка» snap и при наступлении статистического равновесия, которое определяется с помощью метода dynamicAnimatorDidPause(_ animator: UIDynamicAnimator) делегата UIDynamicAnimatorDelegate, запустить на выполнение 3-ую часть анимации.

Добавляем «поведение» snap к нашему классу CardBehavior в специальной функции snap:

 

Переменная var snapPoint — это public переменная, устанавливаемая извне, она определяет точку, в которую мгновенно «полетит» наш динамический объект item. «Поведение» snap, как и другие «поведения», мы добавляем к нашему «поведению» CardBehavior как «дочернее «поведение» в методе addItem, но не сразу, а спустя 2 секунды:

Через  2 секунды мы убираем у нашего динамического объекта item «поведение» столкновения collisionBehavior и добавляем «поведение» «мгновенного снимка» snap с помощью функции snap (item). Это все, что нужно сделать в классе CardBehavior.

Подсказка № 11 советует нам:

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

Теперь нам нужно в Controller «поймать» состояние статического равновесия с помощью делегата UIDynamicAnimatorDelegate динамической анимации…

… и метода dynamicAnimatorDidPause(_ animator: UIDynamicAnimator):

В этом методе мы выполням анимацию переворота карты «лицевой» стороной вниз, поворота ее на 90° и небольшое уменьшение размера с помощью UIView.transitition при условии, что у нас есть временные карты tmpViews, которые пролетели до «стопки сброса» и готовы анимироваться. После завершения этого процесса временная карта tmpCard убирается из «поведения» cardBehavior, с игрового поля boardView и корректируется количество Sets, обнаруженных пользователем.  После завершения анимации мы убираем временную карту tmpCard из «поведения cardBehavior, из superView и из массива tmpViews .

В результате наша функция flyAwayAnimation () существенно упростилась:

Все, что нам нужно сделать для анимации «улетания карт», это скопировать «совпавшую» setCardView карту во временную tmpCard и добавить ее в качестве subview на игровое поле boardView, а также  к «поведению» cardBehavior. Далее работает динамический аниматор animator.

Необходимо отметить еще две вещи. При инициализации динамического аниматора animator Controller устанавливается себя self его делегатом:

Точку snapPoint, в которую мгновенно «полетит» наш динамический объект item необходимо устанавливать в методе viewDidLayourSubviews, так как ее координаты меняются при измении bounds вашего View:

Игра в этом случае будет протекать точно также, как и в случае выполнения «полета» «совпавших» карт в «стопку сбрасывания», то есть автоматическая “сдача” 3-х новых карт начинается свой полет в то же самое время, когда “улетающие” карты “отскакивают” друг от друга и от границ экрана, летят в «стопку сбрасывания» и переворачиваются.

Для случая использования делегата  UIDynamicAnimatorDelegate код представлен на Github в папке  Set IV NoExtra Stasis.

Для случая обычных анимаций без использования делегата UIDynamicAnimatorDelegate код представлен на Github   для iOS 11 и на Github для iOS 12 в папке  Set IV NoExtra.

Пункт 4 обязательный

Вместо того, чтобы использовать жест swipe для “сдачи 3-х дополнительных карт”, просто выполните жест tap на вашей колоде карт.

Колода карт на нашем UI представлена с помощью Image View:

У нас есть Outlet на этот Image View с именем deckImageView, и при его инициализации мы добавляем на него жест tap:

Обработчиком жеста  tap является метод deal3, который раньше был @IBAction, но теперь необходимость в этой кнопке отпала, и он превратился в обычный метод с атрибутом @objc :

Image View колоды обслуживает класс DeckImageView, у которого есть public переменная deckNumberString, позволяющая отображать оставшееся количество карт в колоде:

Мы корректируем эту строку в нашем знаменитом методе updateViewFromModel():

Пункт 5 обязательный

Автоматически выполните “сдачу 3-х дополнительных карт” (то есть симулируйте жест tap на вашей колоде карт), когда обнаружилось совпадение (match).

Автоматическая сдача 3-х карт происходит при замене выбывших «совпавших» 3-х карт на новые, когда пользователь выбрал СЛЕДУЮЩАЯ карту ПОСЛЕ выбора той карты,  которая вызвала 3-х карточное совпадение.

У нас есть лучшее решение, и оно заключается в том, что при замене старых  «совпавших» 3-х карт на новые мы можем им присвоить alpha = 0 и «сдача» новых карт выполнится АВТОМАТИЧЕСКИ:

Пункт 6 обязательный

Не требуется поддерживать “реорганизацию карт” при выполнении жеста rotation из прошлого Задания (смотри Дополнительные пункты).

Мы не будем убирать пока жест rotation в надежде вернуться к нему в Дополнительных пунктах Задания 4.

Мое решение Задания 4 состоит из двух приложений: анимационной игры Set без использования UIDinamicAnimation, которая находится в папке Set IV NoExtra, и  анимационной игры Set с использованием UIDinamicAnimation, которая находится в папке Set IV NoExtra Stasis.

Все это находится на Github для iOS 11 и на Github для iOS 12.

Один комментарий к “Задание 4 Stanford CS 193P Fall 2017. Анимационная игра Set. Решение обязательных пунктов 1-6.

  1. Татьяна, скажите, пожалуйста, когда 3 карты добавляются после совпадения сэта, как сделать, чтобы они летели поверх тех, которые уже на столе — это нужно делать их копии? А то они сейчас пролетают в зависимости от порядка в массиве, какие то над ними, какие то под ними.

Обсуждение закрыто.