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

        }
    }
}
Categories
Software

Investigating SwiftUI Document-based Apps

My Turtle app is a document-based app (iOS and MacOS) was written a long time ago. The iOS version relied heavily on UIDocumentBrowserViewController for managing the documents. After a couple of years of neglect the document management was starting to have problems. The iOS version wasn’t able to create new documents. Also closing and saving a document seemed to fail about half of the time.

Eventually these problems made the app unusable enough that I started weighing my options on how best to get things working again. I first considered doing something incremental, focussing on specific issues with the iOS build and sticking with the UIKit based implementation.

But then I saw a WWDC video demonstrating how to get started building a SwiftUI Document based app. I felt some trepidation that there would be a lot of refactoring from UIDocument to SwiftUI’s Document. The UIDocument subclass included metadata that in hindsight was could/should have been UI state. The original implementation also includes complexity related to supporting one iOS target and one macOS target. Last but not least, Turtle supports two different document formats. (Legacy and Threaded) I’m positive I’ve done a bad job of structuring the code to support these two document formats. Moving to a SwiftUI DocumentGroup would require rebuilding this.

So despite all these reasons to stick with the messy UIKit implementation, I jumped into a SwiftUI implementation. While I still have some functionality gaps, on the whole this conversion has been satisfying and even fun.

Future posts will detail some of the issues I’ve encountered along the way.