Episode #197

Mastering TV Focus

18 minutes
Published on November 16, 2015

This video is only available to subscribers. Get access to this video and 584 others.

In this episode we take a deep look at how focus works with tvOS. We learn how to use UIFocusGuide to redirect focus when the engine cannot find an appropriate element to focus. We also learn some debugging tricks on how to visualize focus moves using Quick Look, and how to use _whyIsThisNotFocusable to troubleshoot lack of focus. Finally we'll learn about custom focus animations and layered images.

Episode Links

Redirecting Focus with UIFocusGuide

First we add a UIFocusGuide to our view. This acts much like a layout guide, except that it can also receive focus. We can use this to redirect focus to another view.

var focusGuide: UIFocusGuide!

override func viewDidLoad() {
    super.viewDidLoad()

    focusGuide = UIFocusGuide()
    view.addLayoutGuide(focusGuide)

    focusGuide.widthAnchor.constraintEqualToAnchor(buttonA.widthAnchor).active = true
    focusGuide.heightAnchor.constraintEqualToAnchor(buttonC.heightAnchor).active = true
    focusGuide.centerXAnchor.constraintEqualToAnchor(buttonA.centerXAnchor).active = true
    focusGuide.centerYAnchor.constraintEqualToAnchor(buttonC.centerYAnchor).active = true
}

Then we override didUpdateFocusInContext:withCoordinator::


override func didUpdateFocusInContext(context: UIFocusUpdateContext,
         withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
    guard let nextFocusedView = context.nextFocusedView else { return }

    switch nextFocusedView {
      case buttonA: focusGuide.preferredFocusedView = buttonC
      case buttonC: focusGuide.preferredFocusedView = buttonA
      default: break
    }
}

Reset Focus

You can't force the focus engine to focus a given element. This is to enforce consistent focus experience across all apps on the Apple TV. Instead, you can only set the preferredFocusedView property and then ask the focus engine to re-evaluate.

@IBAction func resetFocus() {
    setNeedsFocusUpdate()
    updateFocusIfNeeded()
}

If the current view environment doesn't have focus these calls will not do anything.

Table Views & Custom Focus Animations

Using the provided UIFocusAnimationCoordinator we can add our own focus animations:

override func didUpdateFocusInContext(context: UIFocusUpdateContext,
         withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {

    guard customRows else { return }

    if let prevCell = context.previouslyFocusedView as? UITableViewCell {
        coordinator.addCoordinatedAnimations({
            prevCell.transform = CGAffineTransformIdentity
        }, completion:nil)
    }

    if let nextCell = context.nextFocusedView as? UITableViewCell {
       nextCell.clipsToBounds = false
        coordinator.addCoordinatedAnimations({
            nextCell.transform = CGAffineTransformRotate( CGAffineTransformMakeScale(1.2, 1.2), CGFloat(M_PI / 60.0))
        }, completion: {
            UIView.animateWithDuration(0.5, delay: 0, 
                           usingSpringWithDamping: 0.5,
                            initialSpringVelocity: 10,
                                          options: [], animations: {
                nextCell.transform = CGAffineTransformMakeScale(1.1, 1.1)
            }, completion: nil)
        })
    }
}

Collection Views & Image View Cells

Collection views have no builtin focus animation. If your cells are primarily images, you can use a handy property on UIImageView to automatically show focus for the image view when the cell is selected.

Here we're also providing a custom font, transform, and color to the label when focused so that it is legible when focused.

class ImageCollectionViewCell: UICollectionViewCell {
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var label: UILabel!

    override func awakeFromNib() {
        super.awakeFromNib()

        imageView.adjustsImageWhenAncestorFocused = true
    }

    override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
        var color: UIColor!
        var transform: CGAffineTransform!
        var font: UIFont!

        if context.previouslyFocusedView == self {
            color = UIColor.blackColor()
            transform = CGAffineTransformIdentity
            font = UIFont.systemFontOfSize(16)
        } else {
            color = UIColor.whiteColor()
            transform = CGAffineTransformMakeTranslation(0, 40)
            font = UIFont.boldSystemFontOfSize(24)
        }

        coordinator.addCoordinatedAnimations({
            self.label.transform = transform
            self.label.textColor = color
            self.label.font = font
        }, completion: nil)
    }
}

This episode uses Tvos 9.0.