Managing Focus State in SwiftUI

Mar 09, 2023#swiftui

Focus indicates which element in the display recieves the next input. Use view modifiers to indicate which views can receive focus, to detect which view has focus, and to programmatically control focus state.

Navigating a form can be tedious if the app doesn’t assist with focus. When a view is focused, it’s visually activated and ready for interaction. A view type you might associate with focus is a text field: Often, focus is applied to text fields to bring up the keyboard and tip off the user to type in that field next.

FocusState

Use this property wrapper to describe views whose appearance and contents relate to the location of focus in the scene. When focus enters the modified view, the wrapped value of this property updates to match a given prototype value. To allow for cases where focus is completely absent from a view tree, the wrapped value must be either an optional or a Boolean.

Similarly, when focus leaves, the wrapped value of this property resets to nil or false. Setting the property’s value programmatically has the reverse effect, causing focus to move to the view associated with the updated value. Set the focus binding to false or nil as appropriate to remove focus from all bound fields. You can also use this to remove focus from a TextField and thereby dismiss the keyboard.

focused(_:)

Use this modifier to cause the view to receive focus whenever the the condition value is true. You can use this modifier to observe the focus state of a single view, or programmatically set and remove focus from the view.

struct ChooseUsername: View {
  @State private var username: String = ""
  @FocusState private var usernameFieldIsFocused: Bool
  @State private var showUsernameTaken = false

  var body: some View {
    VStack {
      TextField("Choose a username.", text: $username)
        .focused($usernameFieldIsFocused)
      if showUsernameTaken {
        Text("That username is taken. Please choose another.")
      }
      Button("Submit") {
        showUsernameTaken = false
        if !isUserNameAvailable(username: username) {
          usernameFieldIsFocused = true
          showUsernameTaken = true
        }
      }
    }
  }
}

When focus moves to the view, the binding sets the bound value to true. If a caller sets the value to true programmatically, then focus moves to the modified view. When focus leaves the modified view, the binding sets the value to false. If a caller sets the value to false, SwiftUI automatically dismisses focus.

focused(_:equals:)

Use this modifier to cause the view to receive focus whenever the the binding equals the value. Typically, you create an enumeration of fields that may receive focus, bind an instance of this enumeration, and assign its cases to focusable views.

struct LoginForm: View {
  enum Field: Hashable {
    case username
    case password
  }

  @State private var username = ""
  @State private var password = ""
  @FocusState private var focusedField: Field?

  var body: some View {
    Form {
      TextField("Username", text: $username)
        .focused($focusedField, equals: .username)

      SecureField("Password", text: $password)
        .focused($focusedField, equals: .password)

      Button("Sign In") {
        if username.isEmpty {
          focusedField = .username
        } else if password.isEmpty {
          focusedField = .password
        } else {
          handleLogin(username, password)
        }
      }
    }
  }
}

When focus moves to the modified view, the binding sets the bound value to the corresponding match value. If a caller sets the state value programmatically to the matching value, then focus moves to the modified view. When focus leaves the modified view, the binding sets the bound value to nil. If a caller sets the value to nil, SwiftUI automatically dismisses focus.

Auto focus

Auto focus is the effect where the first relevant view automatically receives the focus upon loading the screen. Though subtle, it’s an experience users expect.

.onAppear {
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
    self.focusedField = .username
  }
}

In a form with multiple fields, you’ll always want to support focus for most fields to assist the user in navigating quickly by tapping return on the keyboard.

Managing focus in list views

To manage focus in list views, we can make use of the fact that Swift enums support associated values. This allows us to define an enum that can hold the id of a list element we want to focus:

enum Focusable: Hashable {
  case none
  case row(id: String)
}

struct Reminder: Identifiable {
  var id: String = UUID().uuidString
  var title: String
}

struct FocusableListView: View {
  @State var reminders: [Reminder] = Reminder.samples
  @FocusState var focusedReminder: Focusable?
  
  var body: some View {
    List {
      ForEach($reminders) { $reminder in
        TextField("", text: $reminder.title)
          .focused($focusedReminder, equals: .row(id: reminder.id))
      }
    }
  }
}