Categories
Software

SwiftUI TextField Focus

When a new view gets presented on the screen (for example a password entry view) most of the time it makes sense to start with the focus in the text field. The keyboard should be visible and the user should be able to just start typing. Sure it’s a minor inconvenience to force users to first tap in the field, and then start typing, but heck we’re not savages are we?

UIKit’s UITextfield always had support for programmatically setting focus via UIResponder. [myTextField makeFirstResponder]

I get the sense this functionality has been gradually evolving in SwiftUI. I feel like the current implementation is more complex than the UIResponder model from yesteryear. I don’t want to sound like I’m grumbling here, and I feel like the complexity is needed to stay true to the SwiftUI model (composability, single source of truth, etc.) Having said that, I did need to iterate way more than anticipated.

The first piece is the @FocusState descriptor used to describe a variable in a View struct. There appear to be two ways to use FocusState.

  1. a Binding<Bool> type when there is a single View that either has or does not have focus
  2. a Binding<enum?> type where there are multiple views, and maybe one of them has focus, or maybe none of them have focus.

I should point out that the notion of focus here is broader than just text entry views. Any type of View and have focus, but the expected behaviour for focus in non-textEntry views is beyond the scope of this post.

Here is an example of how to use a Binding<Bool> FocusState

struct AddEntryView: View {
    
    @Binding var unsavedData: String
    @FocusState var addEntryHasFocus: Bool

    var body: some View {
        TextEditor(text: $unsavedData)
            .focused($addEntryHasFocus)
            .defaultFocus($addEntryHasFocus, true)
    }
}

Here is an example of how to use a Binding<enum?> FocusState

struct PasswordView: View {
    enum PasswordField {
        case secure
        case regular
    }
    @FocusState private var passwordField: PasswordField?
    @State var passwordText1: String = ""
    @State var passwordText2: String = ""
    let passwordExists: Bool

    var body: some View {
        VStack() {
            SecureField("Enter password", text: $passwordText1)
                .focused($passwordField, equals: .secure)
            TextField("Create a password", text: $passwordText2)
                .focused($passwordField, equals: .regular)
        }
        .defaultFocus($passwordField, .secure)
    }
}

Unfortunately, my use case is sort of in between these two implementation options. I only want one field, but sometimes I want it to be a TextField, and sometimes I want it to be a SecureField.

struct PasswordView: View {
    @FocusState private var passwordField: ????
    @State var passwordText: String = ""
    let passwordExists: Bool

    var body: some View {
        if passwordExists {
            SecureField("Enter password", text: $passwordText)
                .focused(????)
        } else {
            TextField("Create a password", text: $passwordText)
                .focused(????)
        }
    }
}

Since there will only ever be one field displayed, I naively thought I could just use the same Binding<Bool> var for both SecureField and TextField. Sadly those values just get ignored. So I need to use the enum approach, even there will only ever be a single field.

And just to add a bit of insult to injury, I wasn’t able to get .defaultFocus working. According to an Apple Engineer in the dev forums, I may have uncovered a bug. woo! All that to say, my next attempt looked something like this.

struct PasswordView: View {
    enum PasswordField {
        case secure
        case regular
    }
    @FocusState private var passwordField: PasswordField?
    @State var passwordText: String = ""
    let passwordExists: Bool

    var body: some View {
	VStack() {
            if passwordExists {
                SecureField("Enter password", text: $passwordText)
                    .focused($passwordField, equals: .secure)
            } else {
                TextField("Create a password", text: $passwordText)
                    .focused($passwordField, equals: .regular)
            }
        }
        .onAppear {
            passwordField = passwordExists ? .secure : .regular
        }
    }
}

BUT, I encountered a life cycle issue. .onAppear gets called before the document gets loaded. In my case, the document contents determine whether to show the TextField or the SecureField. So I needed to replace .onAppear with onChange. So here’s an approximation of my final code.

struct PasswordView: View {
    enum PasswordField {
        case secure
        case regular
    }
    @FocusState private var passwordField: PasswordField?
    @State var passwordText: String = ""
    @Binding var document: TurtleDocument

    var body: some View {
	VStack() {
            if document.passwordExists {
                SecureField("Enter password", text: $passwordText)
                    .focused($passwordField, equals: .secure)
            } else {
                TextField("Create a password", text: $passwordText)
                    .focused($passwordField, equals: .regular)
            }
        }
        .onChange(of: document.encryptedData) {
            passwordField = document.passwordExists ? .secure : .regular

        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *