NEW BOOK! Swift Gems: 100+ tips to take your Swift code to the next level. Learn more ...NEW BOOK! Swift Gems:100+ advanced Swift tips. Lear more...

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.

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Check out our new book!

Swift Gems

100+ tips to take your Swift code to the next level

Swift Gems

100+ tips to take your Swift code to the next level

  • Advanced Swift techniques for experienced developers bypassing basic tutorials
  • Curated, actionable tips ready for immediate integration into any Swift project
  • Strategies to improve code quality, structure, and performance across all platforms
  • Practical Swift insights from years of development, applicable from iOS to server-side Swift

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.