SwiftUI focusSection - UIFocusGuide Alternative for SwiftUI?

For most of the tvOS apps, you need to move focus diagonally. This means that the focus navigation doesn't just move horizontally or vertically between elements but it also requires diagonal movement. In the UIKit application, we can achieve this using the UIFocusGuide. Now let's see how you can achieve this in SwiftUI using focusSection view modifier.

1. The Problem

In the following image, there is a row of three buttons and a column of three buttons created using VStack and HStack. Here focus moves automatically between buttons in the same row or same column, but the focus engine doesn't move focus when the user swipes down from "Button 2" or "Button 3". But we did expect the focus to move to "Button 4".

Here is the SwiftUI code for the above design:

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 75) {
            HStack(spacing: 75) {
                Button("Button 1") {}
                Button("Button 2") {}
                Button("Button 3") {}
            }
            HStack(spacing: 75) {
                Button("Button 4") {}
            }
            HStack(spacing: 75) {
                Button("Button 5") {}
            }
        }
        .fixedSize()
        .padding()
    }
}

2. Why it doesn't work?

This doesn't work by default because directional focus is based on the adjacency relationships. When swiping down to move focus, the focus will only move if there is something adjacent and focusable in that direction. Since there is no focusable view adjacent to "Button 2" or "Button 3" on the down, the "Button 4" on the far left is unreachable.

3. The Solution (Add focusSection)

To make "Button 4" focusable from "Button 2" or "Button 3", you need to extend the "Button 4" focusable area. This way it becomes adjacent to the "Button 2" or "Button 3". To make "Button 4" adjacent you need to add a Spacer to HStack that contains the "Button 4".

HStack(spacing: 75) {
    Button("Button 4") {}
    Spacer()
}

Now, to create a larger logical focus target around "Button 4" use the focusSection view modifier to the HStack.

HStack(spacing: 75) {
    Button("Button 4") {}
    Spacer()
}
.focusSection()

With this change, the complete HStack view becomes capable of accepting focus if it contains any focusable subviews.

Now if you run the app, you can move focus from "Button 2" or "Button 3" to "Button 4".

Here is the complete code:

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 75) {
            HStack(spacing: 75) {
                Button("Button 1") {}
                Button("Button 2") {}
                Button("Button 3") {}
            }
            HStack(spacing: 75) {
                Button("Button 4") {}
                Spacer()
            }
            .focusSection()
            HStack(spacing: 75) {
                Button("Button 5") {}
            }
        }
        .fixedSize()
        .padding()
    }
}

Here is the demo video:


You've successfully subscribed to Developer Insider
Great! Next, complete checkout for full access to Developer Insider
Welcome back! You've successfully signed in
Success! Your account is fully activated, you now have access to all content.