ДизайнУха
28
Nov

Как создавать Fluid-интерфейсы для iOS

источникmedium.com
авторNathan Gitter

Как написать код для естественных движений и анимаций для создания «текучего» интерфейса

В рамках WWDC 2018 дизайнеры Apple представили доклад под названием “Designing Fluid Interfaces” и рассказали о дизайнерских решениях, стоящих за жестовым интерфейсом iPhone X.

Презентация компании Apple “Designing Fluid Interfaces” в рамках WWDC18

Этот доклад возглавил топ моих любимых презентаций на WWDC — очень рекомендую посмотреть.

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

Swift-подобный псевдокод из презентации

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

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

Мы создадим восемь (8) интерфейсов. Кнопки, пружинные анимации, кастомные взаимодействия и многое другое!

 

Вот, что мы рассмотрим:

  1. Краткий обзор презентации “Designing Fluid Interfaces”.
  2. Восемь fluid-интерфейсов, стоящая за ними теория дизайна и код для их создания.
  3. Приложения для дизайнеров и разработчиков.

 

Что такое fluid-интерфейсы?

Fluid (или текучий) интерфейс также можно назвать «быстрым», «плавным», «естественным», или «волшебным». Это такой гладкий, слаженный опыт, когда чувствуешь: всё как надо.

В презентации WWDC говорится о fluid-интерфейсах как о «продолжении твоих мыслей» и «продолжении естественного мира». Текучим называют интерфейс, который ведет себя в соответствии с тем, как мыслят люди, а не тем, как думают машины.

Что делает их текучими?

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

Приложения можно закрывать даже во время анимации их запуска

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

Зачем нам вообще нужны текучие интерфейсы?

  1. Текучие интерфейсы улучшают пользовательский опыт и делают каждое взаимодействие быстрым, простым и значимым.
  2. Они дарят пользователю ощущения контроля, которое укрепляет доверие к вашему приложению и вашему бренду.
  3. Их сложно воплотить. Fluid- интерфейс сложно скопировать, и это может стать конкурентным преимуществом.

Интерфейсы

В оставшейся части статьи я покажу вам, как выстроить восемь (8) интерфейсов — и таким образом расскажу обо всех основные темах презентации.

Иконки, представляющие восемь (8) интерфейсов, которые мы рассмотрим

Интерфейс #1: кнопка калькулятора

Кнопка, копирующая поведение кнопок в калькуляторе на iOS.

Ключевые характеристики

  1. Мгновенно подсвечивается при прикосновении.
  2. На неё можно быстро и часто тапать даже во время воспроизведения анимации.
  3. Для отмены нажатия после прикосновения, пользователь может без отрыва увести палец за пределы кнопки.
  4. Коснувшись кнопки пальцем, пользователь может без отрыва увести палец за её пределы и вернуть его обратно,  чтобы отпустить и подтвердить нажатие.

Теория дизайна

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

Слайды из презентации WWDC, показывающие, как исполнение жестов параллельно с мыслями ускоряет действия

Необходимый код

Первым шагом к созданию этой кнопки станет использование подкласса UIControl, а не подкласса UIButton. 
UIButton  тоже подойдет, но поскольку мы переопределяем поведение, нам не пригодятся его стандартные возможности.


CalculatorButton: UIControl {
   public var value: Int = 0 {
       didSet { label.text = “\(value)” }
   }
   private lazy var label: UILabel = { ... }()
}


Затем, привяжем к событиям пользовательского взаимодействия методы touchUp и touchDown (мы создадим их далее), используя константы UIControl.Events для определения событий.

	
addTarget(self, action: #selector(touchDown), for: [.touchDown, .touchDragEnter])
addTarget(self, action: #selector(touchUp), for: [.touchUpInside, .touchDragExit, .touchCancel])

Мы сгруппируем события touchDown и touchDragEnter в одно «событие» под названием touchDown, и мы можем сгруппировать touchUpInside, touchDragExit, и touchCancel в единое событие под названием touchUp.

Описания всех доступных для UIControlEvents событий вы найдёте в этой документации.

Получаем две функции для управления анимациями.


private var animator = UIViewPropertyAnimator()
@objc private func touchDown() {
   animator.stopAnimation(true)
   backgroundColor = highlightedColor
}
@objc private func touchUp() {
   animator = UIViewPropertyAnimator(duration: 0.5, curve: .easeOut, animations: {
       self.backgroundColor = self.normalColor
   })
   animator.startAnimation()
}



В методе touchDown мы отменяем существующую анимацию, если это необходимо, и сразу устанавливаем цвет для выделения (в данном случае это светло-серый).  

В методе touchUp, мы создаем новый аниматор и начинаем анимацию. Использование UIViewPropertyAnimator облегчает отмену анимации подсветки.

Примечание: такое поведение не совсем соответствует поведению кнопок на калькуляторе iOS, где можно коснутся одной кнопки, и активировать нажатие другой кнопки, переведя палец без отрыва на неё Однако, чаще всего, данный вариант является ожидаемым поведением для кнопок на IOS.

Интерфейс #2. Пружинная анимация

Этот интерфейс показывает, как можно создать пружинную анимацию с помощью настройки параметров «затухание» (отскакивание) и «ответ» (скорость).

Ключевые характеристики

  1. Использует дизайн-френдли параметры.
  2. Нет варианта настройки длительности анимации.
  3. Легко прерываема.

Теория дизайна

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

И еще несколько дополнительных советов к процессу создания пружинных анимаций:

  1. Пружины не обязательно должны быть пружинистыми. Использование значения затухание (damping) на уровне 1 создаст анимацию, которая медленно успокаивается без всякого отскока. Большинство анимаций должны иметь значение damping на уровне 1.
  2. Постарайтесь не думать о длительности. В теории, пружина никогда до конца не останавливается, и если вы будете насильно устанавливать длительность, анимация потеряет естественность. Вместо этого поиграйте со значениями затухание (damping) и ответ (response), пока не найдете правильный баланс.
  3. Возможность совершить прерывание — это очень важно. Поскольку пружинные анимации большую часть времени находятся вблизи своего финального состояния, пользователи могут подумать, что анимация уже закончилась, и попытаться взаимодействовать с ней еще раз.

Необходимый код

В UIKit можно создать анимацию пружины с помощью UIViewPropertyAnimator  и объекта UISpringTimingParameters. К сожалению,  не существует никакого инициализатора, который бы позволял выставлять любые значения для damping и response. Ближайший вариант — инициализатор UISpringTimingParameters, для значений  массы (mass), жёсткости (stiffness), затухание (damping), и начальную скорость (initial velocity).

	
UISpringTimingParameters(mass: CGFloat, stiffness: CGFloat, damping: CGFloat, initialVelocity: CGVector)

Нам хотелось бы создать удобный инициализатор, который позволил бы выставлять любые значения для damping и response и подстроил бы их под требуемые параметры  массы (mass), жёсткости (stiffness), и затухания (damping).

Используя немножко знаний в области физике, мы можем составить необходимое нам уравнение:

Находим константу пружины и коэффициент затухания

 

С этим результатом мы можем создать собственный UISpringTimingParameters именно с нашими желаемыми параметрами.

extension UISpringTimingParameters {
   convenience init(damping: CGFloat, response: CGFloat, initialVelocity: CGVector = .zero) {
       let stiffness = pow(2 * .pi / response, 2)
       let damp = 4 * .pi * damping / response
       self.init(mass: 1, stiffness: stiffness, damping: damp, initialVelocity: initialVelocity)
   }
}

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

Физика в основе пружинных анимаций

Хотите глубже погрузиться в тему пружинных анимаций? Почитайте вот эту замечательную статью Кристиана Шнорра: «Проливая свет на пружинные анимации из UIKit».

 

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

 

Интеракция #3: кнопка фонарика

Еще одна кнопка, но как сильно отличается её поведение. Вот имитация поведения кнопки фонарика на экране блокировки iPhone X.

 

 

Ключевые характеристики

  1. Требуется намеренное прикосновение 3D touch.

  2. Подпрыгивание намекает пользователю на необходимый жест.

  3. Тактильная отдача в виде вибрации подтверждает активацию.

 

Теория дизайна

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

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

Необходимый код

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

	
override func touchesMoved(_ touches: Set, with event: UIEvent?) {
   super.touchesMoved(touches, with: event)
   guard let touch = touches.first else { return }
   let force = touch.force / touch.maximumPossibleForce
   let scale = 1 + (maxWidth / minWidth - 1) * force
   transform = CGAffineTransform(scaleX: scale, y: scale)
}

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

Поскольку на кнопку можно нажимать, не активируя её, нам нужно отслеживать текущее состояние кнопки.

	
enum ForceState {
   case reset, activated, confirmed
}
private let resetForce: CGFloat = 0.4
private let activationForce: CGFloat = 0.5
private let confirmationForce: CGFloat = 0.49

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

Для работы с тактильным откликом в UIKit предназначены классы, наследуемые от UIFeedbackGenerator.

	
private let activationFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
private let confirmationFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)

Наконец, для подпрыгивающей анимации можно использовать UIViewPropertyAnimator с кастомизированным инициализатором UISpringTimingParameters, который мы создали ранее.

	
let params = UISpringTimingParameters(damping: 0.4, response: 0.2)
let animator = UIViewPropertyAnimator(duration: 0, timingParameters: params)
animator.addAnimations {
   self.transform = CGAffineTransform(scaleX: 1, y: 1)
   self.backgroundColor = self.isOn ? self.onColor : self.offColor
}
animator.startAnimation()

Интерфейс #4: Rubberbanding или эластичная анимация

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

Ключевые характеристики

  1. Интерфейс всегда реагирует на действие, даже когда оно недоступно.
  2. Десинхронизация движений пользователя и содержимого страницы даёт сигнал о достижении границы.
  3. Амплитуда движения уменьшается при удалении от границы.

Теория дизайна

Эластичная графика (резиновый скролл) — это отличный способ сообщить о недопустимом действии, при этом оставив пользователю ощущение контроля. Мягко обозначив границы, она возвращает пользователя обратно к допустимому состоянию.

Необходимый код

К счастью, эластичную графику встроить просто.

offset = pow(offset, 0.7)

С использованием экспоненты от 0 до 1 смещение страницы происходит тем меньше, чем дальше она находится от состояния покоя. Используйте бóльшую экспоненту для мéньшего движения и мéньшую экспоненту для бóльшего движения.

Ещё немного контекста: этот код обычно встраивается в функцию обратного вызова UIPanGestureRecognizer каждый раз, когда происходит перемещение прикосновения. Cмещение может быть рассчитано через дельту (разницу) между текущим и начальным положением точки прикосновения, и смещение можно применить с трансформацией сдвига.

	
var offset = touchPoint.y - originalTouchPoint.y
offset = offset > 0 ? pow(offset, 0.7) : -pow(-offset, 0.7)
view.transform = CGAffineTransform(translationX: 0, y: offset)

Примечание: У Apple эластичная графика — например, на прокрутке экрана — написана не так. Мне нравится именно этот метод из-за его простоты, но существуют и более сложные функции для разных вариантов поведения.

Интерфейс #5: Пауза во время ускорения

Чтобы открыть переключатель приложений на iPhone X, пользователь делает свайп вверх с нижней части экрана и останавливает движение на полпути, создавая паузу. Интерфейс ниже воссоздает такое поведение.

Ключевые характеристики 

  1. Пауза рассчитывается, исходя из ускорения жеста.
  2. Более быстрая остановка приводит к более быстрому ответу.
  3. Никаких таймеров.

Теория дизайна

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

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

 

Необходимый код

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

private var velocities = [CGFloat]()
private func track(velocity: CGFloat) {
   if velocities.count < numberOfVelocities {
       velocities.append(velocity)
   } else {
       velocities = Array(velocities.dropFirst())
       velocities.append(velocity)
   }
}

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

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

if abs(velocity) > 100 || abs(offset) < 50 { return }
let ratio = abs(firstRecordedVelocity - velocity) / abs(firstRecordedVelocity)
if ratio > 0.9 {
   pauseLabel.alpha = 1
   feedbackGenerator.impactOccurred()
   hasPaused = true
}

Также нужно убедиться, что движение обладает минимальным смещением и скоростью. Если жест потерял более, чем 90% своей скорости, мы считаем, что он остановлен.

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

 

Интерфейс #6: Вознаграждение за усилие

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

Ключевые характеристики

  1. Касание (тап) на панели открывает её без эффекта упругости.
  2. Движение пролистывания по панели открывает её с эффектом прыгучести.
  3. Интерфейс интерактивен, его можно прерывать, и возвращать в предыдущее состояние.

 

Теория дизайна

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

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

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

Необходимый код

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

class InstantPanGestureRecognizer: UIPanGestureRecognizer {
   override func touchesBegan(_ touches: Set, with event: UIEvent) {
       super.touchesBegan(touches, with: event)
       self.state = .began
   }
}

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

	
if yVelocity == 0 {
   animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)
}

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



let fractionRemaining = 1 - animator.fractionComplete
let distanceRemaining = fractionRemaining * closedTransform.ty
if distanceRemaining == 0 {
   animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)
   break
}
let relativeVelocity = abs(yVelocity) / distanceRemaining
	

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

let timingParameters = UISpringTimingParameters(damping: 0.8, response: 0.3, initialVelocity: CGVector(dx: relativeVelocity, dy: relativeVelocity))
let newDuration = UIViewPropertyAnimator(duration: 0, timingParameters: timingParameters).duration
let durationFactor = CGFloat(newDuration / animator.duration)
animator.continueAnimation(withTimingParameters: timingParameters, durationFactor: durationFactor)

Здесь мы создаем новый UIViewPropertyAnimator, чтобы рассчитать время длительности анимации и предоставить верный параметр durationFactor  для её продолжения.

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

 

Интерфейс #7: «Картинка в картинке» в FaceTime

Воссоздание пользовательского интерфейса «картинка в картинке» или PiP (picture in picture) в приложении FaceTime на iOS.

Ключевые характеристики

  1. Легкое, воздушное взаимодействие.

  2. Прогнозируемая позиция основывается на коэффициенте замедления UIScrollView.

  3. Непрерывная анимация, которая учитывает начальную скорость жеста.

Необходимый код

Наша конечная цель — написать нечто вроде этого:

 let params = UISpringTimingParameters(damping: 1, response: 0.4, initialVelocity: relativeInitialVelocity)
let animator = UIViewPropertyAnimator(duration: 0, timingParameters: params)
animator.addAnimations {
   self.pipView.center = nearestCornerPosition
}
animator.startAnimation()

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

Прежде всего, давайте рассчитаем начальную скорость.

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

let relativeInitialVelocity = CGVector(
   dx: relativeVelocity(forVelocity: velocity.x, from: pipView.center.x, to: nearestCornerPosition.x),
   dy: relativeVelocity(forVelocity: velocity.y, from: pipView.center.y, to: nearestCornerPosition.y)
)
func relativeVelocity(forVelocity velocity: CGFloat, from currentValue: CGFloat, to targetValue: CGFloat) -> CGFloat {
   guard currentValue - targetValue != 0 else { return 0 }
   return velocity / (targetValue - currentValue)
}

Разбив скорость на компоненты x и y, мы можем определить относительную скорость для каждого компонента

Затем давайте рассчитаем угол остановки для «картинки в картинке».

Чтобы сделать наш интерфейс естественным и легким, мы спрогнозируем окончательное положение «картинки в картинке» на основе её текущего движения. Если бы она соскользнула и где-то остановилась, где было бы это место?

let decelerationRate = UIScrollView.DecelerationRate.normal.rawValue
let velocity = recognizer.velocity(in: view)
let projectedPosition = CGPoint(
   x: pipView.center.x + project(initialVelocity: velocity.x, decelerationRate: decelerationRate),
   y: pipView.center.y + project(initialVelocity: velocity.y, decelerationRate: decelerationRate)
)
let nearestCornerPosition = nearestCorner(to: projectedPosition)

Мы можем использовать коэффициент замедления UIScrollView,чтобы рассчитать финальное положение покоя. Это важно, потому что оно привязывается к мышечной памяти пользователя, связанной со скроллингом. Если пользователь знает, как далеко пролистывается страница, он может использовать это знание, чтобы интуитивно понять, сколько усилий нужно затратить для перемещения «картинки в картинке» на желаемое место.

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

Мы можем использовать функцию прогноза, представленную в докладе “Designing Fluid Interfaces”, чтобы рассчитать окончательную позицию.

/// Distance traveled after decelerating to zero velocity at a constant rate.
func project(initialVelocity: CGFloat, decelerationRate: CGFloat) -> CGFloat {
   return (initialVelocity / 1000) * decelerationRate / (1 - decelerationRate)
}

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

	
func nearestCorner(to point: CGPoint) -> CGPoint {
   var minDistance = CGFloat.greatestFiniteMagnitude
   var closestPosition = CGPoint.zero
   for position in pipPositions {
       let distance = point.distance(to: position)
       if distance < minDistance {
           closestPosition = position
           minDistance = distance
       }
   }
   return closestPosition
}

В кратце: мы используем коэффициент замедления UIScrollView, чтобы спроектировать движение «картинки в картинке» к её окончательному положению покоя, и рассчитываем относительную скорость, чтобы ввести всё это в UISpringTimingParameters.

Интерфейс #8: Вращение

Применяем идеи из интерфейса «картинка в картинке» к анимации вращения.

 

Ключевые характеристики:

  1. Использует прогноз, чтобы соотноситься со скоростью жеста.

  2. Финальное положение всегда корректное.

Необходимый код

Код здесь очень похож на код предыдущего интерфейса «картинка в картинке». Мы будем использовать те же самые строительные блоки, за одним исключением: заменим функцию nearestCorner на функцию closestAngle.

func project(...) { ... }
func relativeVelocity(...) { ... }
func closestAngle(...) { ... }

Когда мы, наконец, подходим к созданию UISpringTimingParameters, мы должны использовать CGVector  для начальной скорости, даже если наше вращение имеет только один параметр. В любом случае, когда анимируемый объект обладает только одним параметром, пропишите в значении dx желаемую скорость, а в значении dy поставьте ноль.

let timingParameters = UISpringTimingParameters(
   damping: 0.8,
   response: 0.4,
   initialVelocity: CGVector(dx: relativeInitialVelocity, dy: 0)
)

Аниматор сам будет игнорировать значение dy и использовать значение dx, чтобы создать кривую тайминга.

Попробуйте сами! 

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

Демо-приложение текучих интерфейсов, доступно на GitHub!

Применение на практике

Для дизайнеров 

  1. Думайте об интерфейсах как о текучих выразительных средствах, а не как о собрании статических элементов.
  2. Учитывайте анимации и жесты на ранних стадиях дизайн-процесса. Инструменты для проектирования в вебе вроде Sketch — это фантастика, но они не позволяют на полную использовать выразительность устройства.
  3. Делайте прототипы с разработчиками. Найдите разработчиков с хорошим дизайн-мышлением, которые помогут вам сделать прототипы анимаций, жестов и тактильных взаимодействий.

Для разработчиков

  1. Примените практические советы из описания интерфейсов к вашим кастомным компонентам. Подумайте, как можно их скомбинировать новыми и интересными способами.
  2. Обучите своих дизайнеров новым возможностям. Многие не знают всей мощи 3D touch, тактильных взаимодействий, жестов и пружинных анимаций.
  3. Делайте прототипы с дизайнерами. Помогите им увидеть разработанный ими дизайн на реальных устройствах и создайте инструменты для более эффективного дизайн-процесса.