A post about tvOS UIFocus enlightenment and a helper Class that I use to help debug my user interface work.
Above you’ll notice four buttons (a screenshot from my actual Apple TV). You’ll also notice a total of eight purple boxes with dashed outlines. Each is labeled in all capitals. Those are instances of my helper Class (“FocusGuideRepresentation”). You see, UIFocusGuides do not have any visual interface. So when you are deploying them, you’re essentially working in the dark. This helper Class shows you right where the guides are to help you visually lay everything out. Of course, when you get into dynamic situations where buttons are shuffling around, this can help you even more.
The focus management for tvOS works incredibly well when your buttons line up vertically or horizontally. It just works, you don’t need to do anything for that functionality. When they aren’t aligned, you can get into situations where buttons aren’t available through normal navigation. Above, without any UIFocusGuides, a user could move from button One to Two. And back up. That would be it. Buttons Three and Four would be hung out to dry without the user able to navigate to them. That’s why UIFocusGuides exist.
You can think of them as invisible buttons that pass focus to an actual button – based on rules that you provide.
I decided that in addition to being to move up and down with the Siri remote to access the buttons, left and right should also work at the top. A user swiping right from One to get to Three should work. That means 8 guides, as you can see in the diagram. The dashed rule lines show how each guide passes focus. That is a lot of guides, but in the end, the navigation ends up being buttery and simple to use. An application should be a joy to use.
Below is the code for the helper Class, followed by the full code for what you see in the image. Try it out on an Apple TV and see what’s going on and experience how nice it feels getting around.
import UIKit
class FocusGuideRepresentation: UIView {
init(frameSize: CGRect, label: String)
{
super.init(frame: frameSize)
self.backgroundColor = UIColor.blue.withAlphaComponent(0.1)
let myLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height))
myLabel.font = UIFont.systemFont(ofSize: 20)
myLabel.textColor = UIColor.white.withAlphaComponent(0.5)
myLabel.textAlignment = .center
myLabel.text = label.uppercased()
self.addSubview(myLabel)
// Add a dashed rule around myself.
let border = CAShapeLayer()
border.strokeColor = UIColor.white.withAlphaComponent(0.4).cgColor
border.fillColor = nil
border.lineWidth = 1
border.lineDashPattern = [4, 4]
border.path = UIBezierPath(rect: self.bounds).cgPath
border.frame = self.bounds
self.layer.addSublayer(border)
}
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
And, now, the main code for the ViewController. Note: the 4 UIButtons are in the Storyboard and hooked up (as you see the @IBOutlets).
import UIKit
class ViewController: UIViewController {
@IBOutlet var one: UIButton!
@IBOutlet var two: UIButton!
@IBOutlet var three: UIButton!
@IBOutlet var four: UIButton!
var fg1: FocusGuideRepresentation!
var fg2: FocusGuideRepresentation!
var fg3: FocusGuideRepresentation!
var fg4: FocusGuideRepresentation!
var fg5: FocusGuideRepresentation!
var fg6: FocusGuideRepresentation!
var fg7: FocusGuideRepresentation!
var fg8: FocusGuideRepresentation!
override func viewDidLoad() {
super.viewDidLoad()
setUpFocusGuides()
// Pop these back up above the guide representations.
self.view.addSubview(one)
self.view.addSubview(two)
self.view.addSubview(three)
self.view.addSubview(four)
}
func setUpFocusGuides()
{
let firstFocusGuide = UIFocusGuide()
view.addLayoutGuide(firstFocusGuide)
firstFocusGuide.leftAnchor.constraint(equalTo: one.leftAnchor).isActive = true
firstFocusGuide.topAnchor.constraint(equalTo: one.bottomAnchor).isActive = true
firstFocusGuide.heightAnchor.constraint(equalTo: one.heightAnchor).isActive = true
firstFocusGuide.widthAnchor.constraint(equalTo: three.widthAnchor).isActive = true
firstFocusGuide.preferredFocusEnvironments = [three]
let secondFocusGuide = UIFocusGuide()
view.addLayoutGuide(secondFocusGuide)
secondFocusGuide.rightAnchor.constraint(equalTo: three.rightAnchor).isActive = true
secondFocusGuide.bottomAnchor.constraint(equalTo: three.topAnchor).isActive = true
secondFocusGuide.heightAnchor.constraint(equalTo: three.heightAnchor).isActive = true
secondFocusGuide.widthAnchor.constraint(equalTo: three.widthAnchor).isActive = true
secondFocusGuide.preferredFocusEnvironments = [one]
let thirdFocusGuide = UIFocusGuide()
view.addLayoutGuide(thirdFocusGuide)
thirdFocusGuide.leftAnchor.constraint(equalTo: two.leftAnchor).isActive = true
thirdFocusGuide.bottomAnchor.constraint(equalTo: two.topAnchor).isActive = true
thirdFocusGuide.heightAnchor.constraint(equalTo: two.heightAnchor).isActive = true
thirdFocusGuide.widthAnchor.constraint(equalTo: four.widthAnchor).isActive = true
thirdFocusGuide.preferredFocusEnvironments = [four]
let fourthFocusGuide = UIFocusGuide()
view.addLayoutGuide(fourthFocusGuide)
fourthFocusGuide.leftAnchor.constraint(equalTo: four.leftAnchor).isActive = true
fourthFocusGuide.topAnchor.constraint(equalTo: four.bottomAnchor).isActive = true
//fourthFocusGuide.bottomAnchor.constraint(equalTo: two.bottomAnchor).isActive = true
fourthFocusGuide.heightAnchor.constraint(equalTo: four.heightAnchor).isActive = true
fourthFocusGuide.widthAnchor.constraint(equalTo: four.widthAnchor).isActive = true
fourthFocusGuide.preferredFocusEnvironments = [two]
let fifthFocusGuide = UIFocusGuide()
view.addLayoutGuide(fifthFocusGuide)
fifthFocusGuide.leftAnchor.constraint(equalTo: three.leftAnchor).isActive = true
fifthFocusGuide.bottomAnchor.constraint(equalTo: one.bottomAnchor).isActive = true
fifthFocusGuide.heightAnchor.constraint(equalTo: three.heightAnchor).isActive = true
fifthFocusGuide.widthAnchor.constraint(equalTo: three.widthAnchor).isActive = true
fifthFocusGuide.preferredFocusEnvironments = [three]
let sixthFocusGuide = UIFocusGuide()
view.addLayoutGuide(sixthFocusGuide)
sixthFocusGuide.leftAnchor.constraint(equalTo: one.leftAnchor).isActive = true
sixthFocusGuide.bottomAnchor.constraint(equalTo: three.bottomAnchor).isActive = true
sixthFocusGuide.heightAnchor.constraint(equalTo: three.heightAnchor).isActive = true
sixthFocusGuide.widthAnchor.constraint(equalTo: three.widthAnchor).isActive = true
sixthFocusGuide.preferredFocusEnvironments = [one]
let seventhFocusGuide = UIFocusGuide()
view.addLayoutGuide(seventhFocusGuide)
seventhFocusGuide.leftAnchor.constraint(equalTo: four.leftAnchor).isActive = true
seventhFocusGuide.bottomAnchor.constraint(equalTo: two.bottomAnchor).isActive = true
seventhFocusGuide.heightAnchor.constraint(equalTo: four.heightAnchor).isActive = true
seventhFocusGuide.widthAnchor.constraint(equalTo: four.widthAnchor).isActive = true
seventhFocusGuide.preferredFocusEnvironments = [four]
let eighthFocusGuide = UIFocusGuide()
view.addLayoutGuide(eighthFocusGuide)
eighthFocusGuide.leftAnchor.constraint(equalTo: two.leftAnchor).isActive = true
eighthFocusGuide.bottomAnchor.constraint(equalTo: four.bottomAnchor).isActive = true
eighthFocusGuide.heightAnchor.constraint(equalTo: four.heightAnchor).isActive = true
eighthFocusGuide.widthAnchor.constraint(equalTo: four.widthAnchor).isActive = true
eighthFocusGuide.preferredFocusEnvironments = [two]
// To aid in debug placement.
fg1 = FocusGuideRepresentation(frameSize: firstFocusGuide.layoutFrame, label: "first")
fg2 = FocusGuideRepresentation(frameSize: secondFocusGuide.layoutFrame, label: "second")
fg3 = FocusGuideRepresentation(frameSize: thirdFocusGuide.layoutFrame, label: "third")
fg4 = FocusGuideRepresentation(frameSize: fourthFocusGuide.layoutFrame, label: "fourth")
fg5 = FocusGuideRepresentation(frameSize: fifthFocusGuide.layoutFrame, label: "fifth")
fg6 = FocusGuideRepresentation(frameSize: sixthFocusGuide.layoutFrame, label: "sixth")
fg7 = FocusGuideRepresentation(frameSize: seventhFocusGuide.layoutFrame, label: "seventh")
fg8 = FocusGuideRepresentation(frameSize: eighthFocusGuide.layoutFrame, label: "eighth")
self.view.addSubview(fg1)
self.view.addSubview(fg2)
self.view.addSubview(fg3)
self.view.addSubview(fg4)
self.view.addSubview(fg5)
self.view.addSubview(fg6)
self.view.addSubview(fg7)
self.view.addSubview(fg8)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
