Rolf Hendriks
Rolf Hendriks

Reputation: 421

Apple TV - how to implement a sticky focus selection like in UISegmentedControl?

In a custom control, how do you implement a sticky focus selection as seen in UISegmentedControl, UITabBar, UITableView, or UICollectionView? By 'sticky selection' I mean automatically diverting focus to the last focused/selected item when focusing into a container.

How do UISegmentedControl + others implement this themselves? I tried inspecting UISegmentedControl and discovered to my surprise that it is a single large focusable element, not a group of focusable segments. I was not able to find out how UISegmentedControl is able to switch focus between segments given its architecture.

So far the workaround I have is:

At the screen level, maintain each sticky selection. For example if the screen has two custom top menus, the screen maintains two active selections. Override shouldUpdateFocus to detect focus changes into a container/menu with a sticky selection. Return false + call setNeedsFocusUpdate combined with overriding preferredFocusEnvironments to redirect focus to the sticky/selected subview instead.

My solution works, but is unsatisfying for writing reusable custom UIs that maintain sticky selections automatically. Apple was able to make several such custom UIs, so there must be some method I am missing.

Upvotes: 0

Views: 205

Answers (1)

Rolf Hendriks
Rolf Hendriks

Reputation: 421

I found a method that works for reusable controls:

Surround the control in a UIFocusGuide. Then simply use UIFocusGuide.preferredEnvironments to determine which subview receives focus when focus shifts to the container.

There is a caveat that if a container's subviews fill the container, the subviews will be hit instead of the container's surrounding focus guide. So overall, I have:

    self.focusGuide = UIFocusGuide()
    let focusGuideExpansion: CGFloat = 1
    self.addLayoutGuide(focusGuide)
    focusGuide.leadingAnchor.constraint(equalTo:self.leadingAnchor, constant:-focusGuideExpansion).isActive = true
    focusGuide.trailingAnchor.constraint(equalTo:self.trailingAnchor, constant:focusGuideExpansion).isActive = true
    focusGuide.topAnchor.constraint(equalTo:self.topAnchor, constant:-focusGuideExpansion).isActive = true
    focusGuide.bottomAnchor.constraint(equalTo:self.bottomAnchor, constant:focusGuideExpansion).isActive = true

    // default to focusing on the first subview
    focusGuide.preferredFocusEnvironments = [subviews.first!]

Upvotes: 0

Related Questions