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 Source Code 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) } }