Adjust the direction of focus-based navigation in SwiftUI

When the user navigates through focusable views in our app with the tab key, the focus will move in the reading order: first from the leading edge to the trailing edge and then from top down.

While this default behavior is right for many use cases, sometimes we need to customize and redirect the focus movement to fit our custom app design.

For example, we might have an app with two columns of text fields, and we want the user to navigate through the first column first before moving onto the second column. Here is the basic code to demonstrate such UI:

struct ContentView: View {
    @State var todo = [
        TodoTask(text: "Task 1"),
        TodoTask(text: "Task 2")
    ]
    @State var done = [
        TodoTask(text: "Task 3"),
        TodoTask(text: "Task 4")
    ]
    
    var body: some View {
        HStack(spacing: 40) {
            TaskColumn(name: "TODO", tasks: $todo)
            TaskColumn(name: "DONE", tasks: $done)
        }
    }
}

struct TaskColumn: View {
    let name: String
    @Binding var tasks: [TodoTask]
    
    var body: some View {
        VStack {
            Text(name)
                .font(.headline)
            ForEach($tasks) { $task in
                TextField("Task", text: $task.text)
            }
        }
    }
}

struct TodoTask: Identifiable {
    var id = UUID()
    var text = ""
}

If we run our example as it is and try to navigate using the tab key, we'll keep moving from the "TODO" column to the "DONE" column task by task, instead of going through the "TODO" column first and then moving to the "DONE" column.

To adjust this behavior we can apply the new focusSection() modifier to the columns. This modifier was introduced this year and is only available in macOS 13. It indicates that the focus should first move across the children of the focus section, before moving onto the next focusable view.

struct ContentView: View {
    ...
    
    var body: some View {
        HStack(spacing: 40) {
            TaskColumn(name: "TODO", tasks: $todo)
                .focusSection()
            
            TaskColumn(name: "DONE", tasks: $done)
                .focusSection()
        }
    }
}

Now the focus navigation will behave how we want and move through the first column from top to bottom, then move through the second column.

Note that focusSection() doesn't make views focusable, it only groups them into a section for directing focus movement. If we apply it to a hierarchy of non-focusable views, it will just be ignored.

Natalia's new book Integrating SwiftUI into UIKit apps is now available!

  • Discover various ways to add SwiftUI to existing UIKit projects
  • Use Xcode previews when designing and building UI
  • Update your UIKit apps with iOS 16 features such as Swift Charts and Lock Screen widgets
  • Migrate larger parts of your apps to SwiftUI while reusing views and controllers built in UIKit
Find out more...