Using KeyPaths to Drive Programmatic Focus


The .focused(_:equals:) modifier is new in iOS15 and macOS12. It requires us to pass a FocusState and allows programmatically setting and responding to focus changes.


Commonly examples of this in action tend to use a custom enum to enable switching between the fields in a form however in most cases the fields of a form naturally associate themselves with fields in your models. Therefore it is possible to use the KeyPath as the focus value, and in fact, since the value only needs to conform to Hashable we can even use AnyKeyPath as the type so that we can handle focus across multiple fields spanning more than one model.


struct ContentView: View {
  @State
  var project = Project()

  @FocusState
  var focus: AnyKeyPath?

  var body: some View {
    Form {
      TextField("Name", text: $project.name)
      .focused($focus, equals: \Project.name)

      TextEditor(text: $project.body)
      .focused($focus, equals: \Project.body)
    }
  }
}


Adding a submit button to the form lets us validate and if needed focus the user on the field that is missing a value.


Button("Submit") {
  guard project.name != "" else {
    focus = \Project.name
    return
  }
  guard project.body != "" else {
    focus = \Project.body
    return
  }
  // Do something
}


In addition, we can add an onAppear(perform:) operation to the from so that we can also drive focus as soon as the form appears on the screen.


.onAppear {
  guard project.name != "" else {
    focus = \Project.name
    return
  }
  guard project.body != "" else {
    focus = \Project.body
    return
  }
}


Responding to focus changes can also be useful, in this example we can force the focus back to the name field if it is not set by adding an onChange(of:perform:) modifier to the form.


.onChange(of: focus) { newValue in
  if project.name == "" {
    focus = \Project.name
  }
}