Статьи

Взять под контроль движок tvOS Focus

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

К счастью, реализации API-интерфейсов UIKit в tvOS обрабатывают изменение фокуса между представлениями автоматически. Хотя эта встроенная система работает очень хорошо, для определенных макетов и / или целей просмотра иногда может потребоваться вручную управлять механизмом фокусировки.

В этом уроке мы подробно рассмотрим механизм фокусировки tvOS. Вы узнаете, как это работает и как это контролировать, как вы хотите.

Это руководство требует, чтобы вы работали с Xcode 7.3 или выше с последней версией SDK tvOS 9.2. Если вы хотите следовать, вам также нужно скачать стартовый проект с GitHub .

Цель механизма фокуса tvOS — помочь разработчикам сконцентрироваться на уникальном контенте своего приложения, а не реализовывать базовые навигационные функции. Это означает, что, хотя многие пользователи будут использовать Siri Remote от Apple TV, механизм фокусировки автоматически поддерживает все существующие и будущие устройства ввода Apple TV.

Это означает, что как разработчику вам не нужно беспокоиться о том, как пользователь взаимодействует с вашим приложением. Другой важной целью механизма фокусировки является создание согласованного пользовательского интерфейса между приложениями. Из-за этого нет API, позволяющего приложению перемещать фокус.

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

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

  • перемещение фокуса мимо определенных видов, если, например, пользователь быстро проводит пальцем по сенсорной поверхности пульта Apple TV
  • запуск анимации на скоростях, основанных на скорости изменения фокуса
  • воспроизведение звуков навигации при смене фокуса
  • анимация прокрутки автоматически смещается, когда фокус необходимо переместить в текущий экран

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

Если механизм фокусировки находит представление, на которое он может переместить фокус, он уведомляет объекты, соответствующие протоколу UIFocusEnvironment , которые связаны с изменением. Классами UIKit, которые соответствуют протоколу UIFocusEnvironment , являются UIWindow , UIViewController , UIView и UIPresentationController . Механизм фокуса вызывает метод shouldUpdateFocusInContext(_:) всех объектов среды фокуса, которые содержат либо текущее фокусированное представление, либо представление, на которое перемещается фокус. Если какой-либо из этих вызовов метода возвращает false , фокус не изменяется.

Протокол UIFocusEnvironment представляет объект, который известен как среда фокусировки . Протокол определяет свойство preferredFocusView объект FocusFocusView, в котором указывается, куда должен перемещаться фокус, если текущая среда сама становится фокусированной.

Например, объект UIViewController по умолчанию для объекта UIViewController является его корневым представлением. Поскольку каждый объект UIView также может указывать свой предпочтительный вид фокуса, можно создать предпочтительную цепь фокуса . Механизм фокуса tvOS следует за этой цепочкой до тех пор, пока конкретный объект не вернет ни self ни nil из своего свойства preferredFocusView фокуса. Используя эти свойства, вы можете перенаправить фокус по всему пользовательскому интерфейсу, а также указать, какое представление должно быть сфокусировано первым, когда контроллер представления появляется на экране.

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

Обновление фокуса происходит, когда происходит одно из трех событий:

  • пользователь вызывает движение фокуса
  • приложение явно запрашивает обновление фокуса
  • система запускает и автоматическое обновление

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

  • Свойство focusedView текущего объекта UIScreen изменяется на представление, к которому перемещается фокус.
  • Механизм фокуса вызывает didUpdateFocusInContext(_:withAnimationCoordinator:) каждого объекта среды фокуса, участвующего в обновлении фокуса. Это тот же набор объектов, который механизм фокуса проверяет, вызывая метод shouldUpdateFocusInContext(_:) каждого объекта перед обновлением фокуса. Именно в этот момент вы можете добавить собственные анимации для запуска в сочетании с анимациями, связанными с фокусом, которые предоставляет система.
  • Все согласованные анимации, как системные, так и пользовательские, запускаются одновременно.
  • Если представление, к которому перемещается фокус, в настоящее время находится за пределами экрана и в режиме прокрутки, система прокручивает представление на экране, так что представление становится видимым для пользователя.

Чтобы вручную обновить фокус в пользовательском интерфейсе, вы можете вызвать метод setNeedsFocusUpdate() любого объекта среды фокусировки. Это сбрасывает фокус и перемещает его обратно в preferredFocusView фокус среды.

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

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

Чтобы расширить механизм фокуса, мы собираемся реализовать обходное поведение. Наше текущее приложение имеет сетку из шести кнопок, как показано на скриншоте ниже.

Настройка проекта

То, что мы собираемся сделать, — это позволить пользователю переместить фокус вправо, с кнопок 3 и 6, и переместить фокус обратно на кнопки 1 и 4 соответственно. Так как механизм фокусировки игнорирует любые невидимые виды, это нельзя сделать, вставив невидимый UIView (включая представление с шириной и высотой 0) и изменив его свойство preferredFocusedView объект UIView .

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

  • preferredFocusedView : это свойство работает, как я описал ранее. Вы можете думать об этом как о представлении, к которому вы хотите перенаправить руководство по фокусировке.
  • enabled : это свойство позволяет включать или отключать фокусировочное руководство.

В вашем проекте откройте ViewController.swift и реализуйте метод viewDidAppear(_:) класса ViewController как показано ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)
     
    let rightButtonIds = [3, 6]
    for buttonId in rightButtonIds {
        if let button = buttonWithTag(buttonId) {
            let focusGuide = UIFocusGuide()
            view.addLayoutGuide(focusGuide)
            focusGuide.widthAnchor.constraintEqualToAnchor(button.widthAnchor).active = true
            focusGuide.heightAnchor.constraintEqualToAnchor(button.heightAnchor).active = true
            focusGuide.leadingAnchor.constraintEqualToAnchor(button.trailingAnchor, constant: 60.0).active = true
            focusGuide.centerYAnchor.constraintEqualToAnchor(button.centerYAnchor).active = true
            focusGuide.preferredFocusedView = buttonWithTag(buttonId-2)
        }
    }
     
    let leftButtonIds = [1, 4]
    for buttonId in leftButtonIds {
        if let button = buttonWithTag(buttonId) {
            let focusGuide = UIFocusGuide()
            view.addLayoutGuide(focusGuide)
            focusGuide.widthAnchor.constraintEqualToAnchor(button.widthAnchor).active = true
            focusGuide.heightAnchor.constraintEqualToAnchor(button.heightAnchor).active = true
            focusGuide.trailingAnchor.constraintEqualToAnchor(button.leadingAnchor, constant: -60.0).active = true
            focusGuide.centerYAnchor.constraintEqualToAnchor(button.centerYAnchor).active = true
            focusGuide.preferredFocusedView = buttonWithTag(buttonId+2)
        }
    }
}

В viewDidAppear(_:) мы создаем направляющие фокуса справа от кнопок 3 и 6 и слева от кнопок 1   и 4. Поскольку эти направляющие фокусировки представляют фокусируемую область в пользовательском интерфейсе, они должны иметь заданную высоту и ширину. С помощью этого кода мы делаем регионы того же размера, что и другие кнопки, чтобы логика импульса механизма фокусировки соответствовала видимым кнопкам.

Чтобы проиллюстрировать, как работают скоординированные анимации, мы обновляем свойство alpha кнопок при изменении фокуса. В ViewController.swift реализуйте метод didUpdateFocusInContext(_:withAnimationCoordinator:) в классе ViewController :

01
02
03
04
05
06
07
08
09
10
11
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
    super.didUpdateFocusInContext(context, withAnimationCoordinator: coordinator)
     
    if let focusedButton = context.previouslyFocusedView as?
        coordinator.addCoordinatedAnimations({
            focusedButton.alpha = 0.5
        }, completion: {
            // Run completed animation
        })
    }
}

Параметр context для didUpdateFocusInContext(_:withAnimationCoordinator:) является объектом UIFocusUpdateContext который имеет следующие свойства:

  • previouslyFocusedView : ссылается на вид, с которого фокус перемещается
  • nextFocusedView : ссылается на представление, к nextFocusedView перемещается фокус
  • focusHeading : UIFocusHeading перечисления UIFocusHeading представляющее направление, в котором перемещается фокус

С реализацией didUpdateFocusInContext(_:withAnimationCoordinator:) , мы добавили скоординированную анимацию, чтобы изменить альфа-значение ранее сфокусированной кнопки на 0,5, а значение текущей в данный момент кнопки — 1,0.

Запустите приложение в симуляторе и переместите фокус между кнопками в пользовательском интерфейсе. Вы можете видеть, что текущая фокусированная кнопка имеет альфа-значение 1,0, а ранее сфокусированная кнопка имеет альфа-значение 0,5.

Прозрачные кнопки

Первое закрытие метода addCoordinatedAnimations(_:completion:) работает аналогично обычному UIView анимации UIView . Разница в том, что он наследует свою продолжительность и функцию синхронизации от движка фокуса.

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

1
2
3
4
5
6
7
// Running custom timed animation
let duration = UIView.inheritedAnimationDuration()
UIView.animateWithDuration(duration/2.0, delay: 0.0, options: .OverrideInheritedDuration, animations: {
    // Animations
}, completion: { (completed: Bool) in
    // Completion block
})

Используя класс UIFocusGuide и используя пользовательские анимации, вы можете расширить стандартное поведение механизма фокуса tvOS в соответствии с вашими потребностями.

Как я упоминал ранее, при принятии решения о том, следует ли перемещать фокус из одного представления в другое, механизм фокусировки вызывает метод shouldUpdateFocusInContext(_:) для каждой задействованной среды фокусировки. Если какой-либо из этих вызовов метода возвращает false , фокус не изменяется.

В нашем приложении мы собираемся переопределить этот метод в классе ViewController чтобы фокус не мог быть перемещен вниз, если в данный момент фокусируемая кнопка имеет значение 2 или 3. Чтобы сделать это, реализуйте shouldUpdateFocusInContext(_:) в классе ViewController как показано ниже :

01
02
03
04
05
06
07
08
09
10
11
override func shouldUpdateFocusInContext(context: UIFocusUpdateContext) -> Bool {
    let focusedButton = context.previouslyFocusedView as?
     
    if focusedButton == buttonWithTag(2) ||
        if context.focusHeading == .Down {
            return false
        }
    }
     
    return super.shouldUpdateFocusInContext(context)
}

В shouldUpdateFocusInContext(_:) мы сначала проверяем, является ли ранее сфокусированное представление кнопкой 2 или 3. Затем мы проверяем заголовок фокуса. Если заголовок равен Down , мы возвращаем false чтобы текущий фокус не изменился.

Запустите ваше приложение в последний раз. Вы не можете переместить фокус вниз с кнопок 2 и 3 на кнопки 5 и 6.

Теперь вам должно быть удобно управлять и работать с движком фокуса tvOS. Теперь вы знаете, как работает механизм фокусировки и как вы можете управлять им, чтобы удовлетворить любые потребности ваших собственных приложений Apple TV.

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