This article will explore experiences, tips and considerations related to SwiftUI TextField events, focus switches, keyboard Settings, and more.

The original post was posted on my blog wwww.fatbobman.com

Public number: [Swift Notepad for elbow]

The event

onEditingChanged

When TextField is in focus (and editable), onEditingChanged calls the given method and passes true; When TextField loses focus, the method is called again and false is passed.

struct OnEditingChangedDemo:View{
    @State var name = ""
    var body: some View{
        List{
            TextField("name:",text:$name,onEditingChanged: getFocus)
        }
    }

    func getFocus(focused:Bool) {
        print("get focus:\(focused ? "true" : "false")")}}Copy the code

The name of this parameter is confusing to the user. Do not use onEditingChanged to determine whether the user entered the content.

In iOS 15, the new constructor that supports ParseableFormatStyle does not provide this parameter, so textFields using the new Formatter need to use other means to determine whether they have gained or lost focus.

onCommit

OnCommit (which cannot be triggered by code emulation) is triggered when the user presses (or clicks) the Return key during input. If the user does not hit the Return key (such as switching directly to another TextField), onCommit will not fire. TextField loses focus when onCommit is triggered.

struct OnCommitDemo:View{
    @State var name = ""
    var body: some View{
        List{
            TextField("name:",text: $name,onCommit: {print("commit")}}}}Copy the code

If you need to judge user input after user input, it is best to combine onCommit and onEdtingChanged. If you want to process the user’s input data in real time, please refer to SwiftUI TextField Advanced – Format and verification.

OnCommit applies to SecureField as well.

In iOS 15, the new constructor supporting ParseableFormatStyle does not provide this parameter, and the new onSubmit can be used to achieve the same effect.

onSubmit

OnSubmit is a new feature in SwiftUI 3.0. OnCommit and onEditingChanged are descriptions of each TextField’s own state, while onSubmit can manage and schedule multiple Textfields in a view from a higher perspective.

// Definition of onSubmit
extension View {
    public func onSubmit(of triggers: SubmitTriggers = .text, _ action: @escaping(() - >Void)) -> some View
}
Copy the code

The following code implements the same behavior as onCommit above:

struct OnSubmitDemo:View{
    @State var name = ""
    var body: some View{
        List{
            TextField("name:",text: $name)
                .onSubmit {
                    print("commit")}}}}Copy the code

OnSubmit triggers the same conditions as onCommit, requiring the user to actively click return.

OnSubmit also works with SecureField.

Scope and nesting

OnSubmit is implemented by setting the environment value TriggerSubmitActio (not yet available to developers), so onSubmit is scoped (passed up the view tree) and nested.

struct OnSubmitDemo: View {
    @State var text1 = ""
    @State var text2 = ""
    @State var text3 = ""
    var body: some View {
        Form {
            Group {
                TextField("text1", text: $text1)
                    .onSubmit { print("text1 commit")}TextField("text2", text: $text2)
                    .onSubmit { print("text2 commit") }
            }
            .onSubmit { print("textfield in group commit")}TextField("text3", text: $text3)
                .onSubmit { print("text3 commit") }
        }
        .onSubmit { print("textfield in form commit1") }
        .onSubmit { print("textfield in form commit2")}}}Copy the code

When TextField (text1) commit, the console output is

textfield in form commit2
textfield in form commit1
textfield in group commit
text1 commit
Copy the code

Notice that the order of calls is from outer in.

Scoped

You can use submitScope to block the scope (limiting further passing in the view tree). For example, in the code above, add submitScope after Group

            Group {
                TextField("text1", text: $text1)
                    .onSubmit { print("text1 commit")}TextField("text2", text: $text2)
                    .onSubmit { print("text2 commit") }
            }
            .submitScope()  // Block scope
            .onSubmit { print("textfield in group commit")}Copy the code

When TextField1 commit, the console output is

text1 commit
Copy the code

At this point, onSubmit’s scope will be limited to the Group.

When there are multiple TextFields in a view, the combination of onSubmit and FocusState (described below) provides a very good user experience.

Support for Searchable

The new search box in iOS 15 also triggers onSubmit when you click Return, but triggers is set to Search:

struct OnSubmitForSearchableDemo:View{
    @State var name = ""
    @State var searchText = ""
    var body: some View{
        NavigationView{
            Form{
                TextField("name:",text:$name)
                    .onSubmit {print("textField commit")}
            }
            .searchable(text: $searchText)
            .onSubmit(of: .search) { // 
                print("searchField commit")}}}}Copy the code

Note that SubmitTriggers is of type OptionSet, and onSubmit passes the values contained in SubmitTriggers continuously through the environment in the view tree. Delivery terminates when the received SubmitTriggers value is not included in the SubmitTriggers in the onSubmit setting. Simply put, onSubmit(of:.search) blocks the commit state generated by TextFiled. And vice versa.

For example, if we add an onSubmt(of:.text) to our searchable, the code above will not respond to the Commit event of the TextField.

            .searchable(text: $searchText)
            .onSubmit(of: .search) {
                print("searchField commit1")
            }
            .onSubmit {print("textField commit")} // Unable to trigger, blocked by search
Copy the code

Therefore, when processing TextFiled and search boxes at the same time, special attention should be paid to the call sequence between them.

You can support both TextField and search fields in an onSubmit with the following code:

.onSubmit(of: [.text, .search]) {
  print("Something has been submitted")}Copy the code

The following code also does not fire because onSubmit(of: Search) is placed before the searchable.

        NavigationView{
            Form{
                TextField("name:",text:$name)
                    .onSubmit {print("textField commit")}
            }
            .onSubmit(of: .search) { // Will not trigger
                print("searchField commit1")
            }
            .searchable(text: $searchText)}Copy the code

The focus of

Before iOS 15 (Moterey), SwiftUI didn’t provide a way for TextField to get focus (e.g., becomeFirstResponder), so for quite some time developers had to implement similar features through non-Swiftui means.

In SwiftUI 3.0, Apple provided developers with a much better solution than expected, similar to onSubmit, to unify focus judgment and management of TextFields in a view from a higher view level.

Basic usage

SwiftUI provides a new wrapper for the FocusState property to help us determine whether the TextField in the view is in focus. Associate FocusState with a specific TextField by focusing.

struct OnFocusDemo:View{
    @FocusState var isNameFocused:Bool
    @State var name = ""
    var body: some View{
        List{
            TextField("name:",text:$name)
                .focused($isNameFocused)
        }
        .onChange(of:isNameFocused){ value in
            print(value)
        }
    }
}
Copy the code

The code above sets isNameFocused to true when TextField gains focus and false when it loses focus.

For multiple Textfields in the same view, you can create multiple FocusStates to associate the corresponding Textfields, for example:

struct OnFocusDemo:View{
    @FocusState var isNameFocused:Bool
    @FocusState var isPasswordFocused:Bool
    @State var name = ""
    @State var password = ""
    var body: some View{
        List{
            TextField("name:",text:$name)
                .focused($isNameFocused)
            SecureField("password:",text:$password)
                .focused($isPasswordFocused)
        }
        .onChange(of:isNameFocused){ value in
            print(value)
        }
        .onChange(of:isPasswordFocused){ value in
            print(value)
        }
    }
}
Copy the code

The above approach becomes cumbersome and unmanageable when you have more TextFields in a view. Fortunately, FocusState supports not only booleans, but also any hash type. We can use Hashable protocol compliant enumerations to unify the focus of multiple TextFields in a view. The following code will do the same as above:

struct OnFocusDemo:View{
    @FocusState var focus:FocusedField?
    @State var name = ""
    @State var password = ""
    var body: some View{
        List{
            TextField("name:",text:$name)
                .focused($focus, equals: .name)
            SecureField("password:",text:$password)
                .focused($focus,equals: .password)
        }
        .onChange(of: focus, perform: {print($0)})}enum FocusedField:Hashable{
        case name,password
    }
}
Copy the code

Gives the specified TextField focus immediately after the view is displayed

With FocusState, you can easily make the specified TextField focus and pop up the keyboard immediately after the view is displayed:

struct OnFocusDemo:View{
    @FocusState var focus:FocusedField?
    @State var name = ""
    @State var password = ""
    var body: some View{
        List{
            TextField("name:",text:$name)
                .focused($focus, equals: .name)
            SecureField("password:",text:$password)
                .focused($focus,equals: .password)
        }
        .onAppear{
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){
                focus = .name
            }
        }
    }

    enum FocusedField:Hashable{
        case name,password
    }
}
Copy the code

Assignment is invalid during view initialization. Even in onAppear, there has to be a delay for the TextField to come into focus.

Switch focus between multiple TextFields

By using a combination of Focused and onSubmit, we can automatically switch focus to the next TextField when the user finishes typing in one TextField (click return).

struct OnFocusDemo:View{
    @FocusState var focus:FocusedField?
    @State var name = ""
    @State var email = ""
    @State var phoneNumber = ""
    var body: some View{
        List{
            TextField("Name:",text:$name)
                .focused($focus, equals: .name)
                .onSubmit {
                    focus = .email
                }
            TextField("Email:",text:$email)
                .focused($focus,equals: .email)
                .onSubmit {
                    focus = .phone
                }
            TextField("PhoneNumber:",text:$phoneNumber)
                .focused($focus, equals: .phone)
                .onSubmit {
                    if !name.isEmpty && !email.isEmpty && !phoneNumber.isEmpty {
                        submit()
                    }
                }
        }
    }

    private func submit(a){
        // submit all infos
        print("submit")}enum FocusedField:Hashable{
        case name,email,phone
    }
}
Copy the code

The above code can also take advantage of the passing feature of onSubmit to look like this:

        List {
            TextField("Name:", text: $name)
                .focused($focus, equals: .name)
            TextField("Email:", text: $email)
                .focused($focus, equals: .email)
            TextField("PhoneNumber:", text: $phoneNumber)
                .focused($focus, equals: .phone)
        }
        .onSubmit {
            switch focus {
            case .name:
                focus = .email
            case .email:
                focus = .phone
            case .phone:
                if !name.isEmpty, !email.isEmpty, !phoneNumber.isEmpty {
                    submit()
                }
            default:
                break}}Copy the code

We can also change the focus forward or jump to another specific TextField, combined with a set of on-screen buttons (such as auxiliary keyboard views) or shortcuts.

Use the shortcut keys to get focus

When there are multiple Textfields (including SecureFields) in a view, we can simply switch the focus of textfields in sequence using the Tab key, but SwiftUI does not directly provide shortcuts to focus a TextField. This capability is available on iPad and MacOS by combining FocusState and keyboardShortcut.

Create shortcut key bindings for ‘focused’ :

public extension View {
    func focused(_ condition: FocusState<Bool>.Binding.key: KeyEquivalent.modifiers: EventModifiers = .command) -> some View {
        focused(condition)
            .background(Button("") {
                condition.wrappedValue = true
            }
            .keyboardShortcut(key, modifiers: modifiers)
            .hidden()
            )
    }

    func focused<Value> (_ binding: FocusState<Value>.Binding.equals value: Value.key: KeyEquivalent.modifiers: EventModifiers = .command) -> some View where Value: Hashable {
        focused(binding, equals: value)
            .background(Button("") {
                binding.wrappedValue = value
            }
            .keyboardShortcut(key, modifiers: modifiers)
            .hidden()
            )
    }
}
Copy the code

Calling code:

struct ShortcutFocusDemo: View {
    @FocusState var focus: FouceField?
    @State private var email = ""
    @State private var address = ""
    var body: some View {
        Form {
            TextField("email", text: $email)
                .focused($focus, equals: .email, key: "t")
            TextField("address", text: $address)
                .focused($focus, equals: .address, key: "a", modifiers: [.command, .shift,.option])
        }
    }

    enum FouceField: Hashable {
        case email
        case address
    }
}
Copy the code

⌘ + T, the Email TextField will gain focus, and ⌘ + ⌥ + ⇧ + A, the Address TextField will gain focus.

The above code does not work well on the iPad emulator (sometimes not activated), please use the real machine to test.

Create your own onEditingChanged

The best way to determine the FocusState of a single TextField is still to use onEditingChanged, but in some cases where onEditingChanged is not available (such as with a new Formatter), we can use FocusState to achieve a similar effect.

  • Judge a single TextField
public extension View {
    func focused(_ condition: FocusState<Bool>.Binding.onFocus: @escaping (Bool) - >Void) -> some View {
        focused(condition)
            .onChange(of: condition.wrappedValue) { value in
                onFocus(value = = true)}}}Copy the code

Call:

struct onEditingChangedFocusVersion:View{
    @FocusState var focus:Bool
    @State var price = 0
    var body: some View{
        Form{
            TextField("Price:",value:$price,format: .number)
                .focused($focus){ focused in
                    print(focused)
                }
        }
    }
}
Copy the code
  • Judge multiple TextFields

To avoid multiple calls after the TextField loses focus, we need to save the FocusState value of the TextField that gained focus last time at the view level.

public extension View {
    func storeLastFocus<Value: Hashable> (current: FocusState<Value? >.Binding.last: Binding<Value? >) -> some View {
        onChange(of: current.wrappedValue) { _ in
            if current.wrappedValue ! = last.wrappedValue {
                last.wrappedValue = current.wrappedValue
            }
        }
    }

    func focused<Value> (_ binding: FocusState<Value>.Binding.equals value: Value.last: Value? .onFocus: @escaping (Bool) - >Void) -> some View where Value: Hashable {
        return focused(binding, equals: value)
            .onChange(of: binding.wrappedValue) { focusValue in
                if focusValue = = value {
                    onFocus(true)}else if last = = value { // Only triggers once
                    onFocus(false)}}}}Copy the code

Call:

struct OnFocusView: View {
    @FocusState private var focused: Focus?
    @State private var lastFocused: Focus?
    @State private var name = ""
    @State private var email = ""
    @State private var address = ""
    var body: some View {
        List {
            TextField("Name:", text: $name)
                .focused($focused, equals: .name, last: lastFocused) {
                    print("name:".$0)}TextField("Email:", text: $email)
                .focused($focused, equals: .email, last: lastFocused) {
                    print("email:".$0)}TextField("Address:", text: $address)
                .focused($focused, equals: .address, last: lastFocused) {
                    print("address:".$0)
                }
        }
        .storeLastFocus(current: $focused, last: $lastFocused) // Save the last focsed value
    }

    enum Focus {
        case name, email, address
    }
}
Copy the code

The keyboard

TextField inevitably involves dealing with a soft keyboard, and this section describes several keyboard-related examples.

The keyboard type

In iPhone, we can use keyboardType to set the type of soft keyboard to facilitate user input or limit the range of input characters.

Such as:

struct KeyboardTypeDemo:View{
    @State var price:Double = 0
    var body: some View{
        Form{
            TextField("Price:",value:$price,format: .number.precision(.fractionLength(2)))
                .keyboardType(.decimalPad) // A numeric keypad that supports decimal points}}}Copy the code

Currently, 11 keyboard types are supported:

  • asciiCapable

    ASCII keyboard

  • numbersAndPunctuation

    Numbers and punctuation

  • URL

    Easy to enter the URL, including characters and., /,.com

  • numberPad

    Use numeric keypads (0-9, ۰-۹, ०-९, etc.) for regional Settings. Works with positive integers or PINS

  • phonePad

    Numbers and other symbols used on telephones, such as *#+

  • namePhonePad

    Easy to input text and phone numbers. Character states are similar to asciiCapable and number states are similar to numberPad

  • emailAddress

    AssiiCapable keyboard for easy typing @

  • decimalPad

    The numberPad containing the decimal point, as shown in the figure above

  • twitter

    AsciiCapable keyboard with @#

  • webSearch

    AsciiCapable keyboard containing., return key marked Go

  • asciiCapableNumberPad

    AsciiCapable keyboard that contains numbers

Although Apple has a number of preset keyboard modes to choose from, in some cases it still doesn’t work.

For example, numberPad and decimalPad have no – and return. Prior to SwiftUI 3.0, we had to draw on the main view or use a non-Swiftui way to solve the problem. In SwiftUI 3.0, this problem is no longer difficult due to the addition of the ability to set up the keyboard assist view natively (more on that below).

Get advice from TextContentType

When using some iOS apps, the soft keyboard will automatically prompt us the content we need to enter, such as phone number, email, verification code and so on. These are the effects of using textContentType.

By setting the TextField to UITextContentType, the system intelligently deduces what it might want to type and displays prompts as it enters.

The following code will allow keystrings when typing passwords:

struct KeyboardTypeDemo: View {
    @State var password = ""
    var body: some View {
        Form {
            SecureField("", text: $password)
                .textContentType(.password)
        }
    }
}
Copy the code

The following code will prompt you to enter your email address by looking for similar addresses in your address book and email:

struct KeyboardTypeDemo: View {
    @State var email = ""
    var body: some View {
        Form {
            TextField("", text: $email)
                .textContentType(.emailAddress)
        }
    }
}
Copy the code

UITextContentType UITextContentType UITextContentType UITextContentType UITextContentType

  • password
  • Name options such as name, givenName, middleName, and so on
  • Address options such as addressCity, fullStreetAddress, postalCode, and so on
  • telephoneNumber
  • emailAddress
  • OneTimeCode (Verification code)

Testing textContentType is best done on a real machine; the emulator does not support certain items or does not have enough information to provide.

Cancel the keyboard

In some cases, after the user has finished typing, we need to cancel the display of the soft keyboard to allow more display space. Some keyboard types do not have a return key, so we need to programmatically make the keyboard disappear.

In addition, sometimes to improve the interactive experience, we can expect the user to cancel the keyboard by clicking on another area of the screen or scrolling through a list after typing, without hitting the Return button. You also need to programmatically make the keyboard disappear.

  • Cancel the keyboard with FocusState

    If you set the corresponding FocusState for TextField, you can cancel the keyboard by setting this value to false or nil

struct HideKeyboardView: View {
    @State private var name = ""
    @FocusState private var nameIsFocused: Bool

    var body: some View {
        Form {
            TextField("Enter your name", text: $name)
                .focused($nameIsFocused)

            Button("dismiss Keyboard") {
                nameIsFocused = false}}}}Copy the code
  • Other situations

    More often, we can cancel the keyboard directly with UIkit methods

UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
Copy the code

For example, the following code will cancel the keyboard when the user drags a view:

struct ResignKeyboardOnDragGesture: ViewModifier {
    var gesture = DragGesture().onChanged { _ in
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)}func body(content: Content) -> some View {
        content.gesture(gesture)
    }
}

extension View {
    func dismissKeyboard(a) -> some View {
        return modifier(ResignKeyboardOnDragGesture()}}struct HideKeyboardView: View {
    @State private var name = ""
    var body: some View {
        Form {
            TextField("Enter your name", text: $name)
        }
        .dismissKeyboard()
    }
}
Copy the code

Keyboard assisted view

Create from the Toolbar

In SwiftUI 3.0, we can use ToolbarItem(Placement:.keyboard, Content: View) to create an auxiliary View (inputAccessoryView) from the keyboard.

By entering an auxiliary view, you can solve many problems that were previously difficult to deal with without providing more means for interaction.

The following code adds a positive/negative conversion and a confirmation button when entering a floating point number:

import Introspect
struct ToolbarKeyboardDemo: View {
    @State var price = ""
    var body: some View {
        Form {
            TextField("Price:", text: $price)
                .keyboardType(.decimalPad)
                .toolbar {
                    ToolbarItem(placement: .keyboard) {
                        HStack {
                            Button("- / +") {
                                if price.hasPrefix("-") {
                                    price.removeFirst()
                                } else {
                                    price = "-" + price
                                }
                            }
                            .buttonStyle(.bordered)
                            Spacer(a)Button("Finish") {
                                UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
                                // do something
                            }
                            .buttonStyle(.bordered)
                        }
                        .padding(.horizontal, 30)}}}}}Copy the code

Unfortunately, setting the input auxiliary view with ToolbarItem currently has the following drawbacks:

  • Limited display content

    The height is fixed and the full display area of the auxiliary view is not available. Like other types of toolbars, SwiftUI interferes with the typography of the content.

  • You cannot set secondary views for multiple TextFields in the same view

    A slightly more complex judgment syntax cannot be used in ToolbarItem. If you set different TextFields separately, SwiftUI will display all the contents together.

SwiftUI’s current meddling and handling of the Toolbar content is a bit over the top. The intention is good, helping developers organize buttons more easily and automatically optimize and display them for different platforms. However, the Toolbar and ToolbarItem ResultBuilder are too limited to make more complex logical decisions. Integrating keyboard-assisted views into the toolbar’s logic is also somewhat confusing.

Created by UIKit

At this stage, creating keyboard assisted views through UIKit is still the best solution under SwiftUI. Not only can you gain complete view display control, but also multiple Textfields in the same view can be set separately.

extension UIView {
    func constrainEdges(to other: UIView) {
        translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            leadingAnchor.constraint(equalTo: other.leadingAnchor), 
            trailingAnchor.constraint(equalTo: other.trailingAnchor), 
            topAnchor.constraint(equalTo: other.topAnchor), 
            bottomAnchor.constraint(equalTo: other.bottomAnchor), 
        ])
    }
}

extension View {
    func inputAccessoryView<Content: View> (@ViewBuilder content: @escaping() - >Content) -> some View {
        introspectTextField { td in
            let viewController = UIHostingController(rootView: content())
            viewController.view.constrainEdges(to: viewController.view)
            td.inputAccessoryView = viewController.view
        }
    }
    
    func inputAccessoryView<Content: View> (content: Content) -> some View {
        introspectTextField { td in
            let viewController = UIHostingController(rootView: content)
            viewController.view.constrainEdges(to: viewController.view)
            td.inputAccessoryView = viewController.view
        }
    }
}
Copy the code

Call:

struct OnFocusDemo: View {
    @FocusState var focus: FocusedField?
    @State var name = ""
    @State var email = ""
    @State var phoneNumber = ""
    var body: some View {
        Form {
            TextField("Name:", text: $name)
                .focused($focus, equals: .name)
                .inputAccessoryView(content: accessoryView(focus: .name))

            TextField("Email:", text: $email)
                .focused($focus, equals: .email)
                .inputAccessoryView(content: accessoryView(focus: .email))

            TextField("PhoneNumber:", text: $phoneNumber)
                .focused($focus, equals: .phone)
        }
        .onSubmit {
            switch focus {
            case .name:
                focus = .email
            case .email:
                focus = .phone
            case .phone:
                if !name.isEmpty, !email.isEmpty, !phoneNumber.isEmpty {}
            default:
                break}}}}struct accessoryView: View {
    let focus: FocusedField?
    var body: some View {
        switch focus {
        case .name:
            Button("name") {}.padding(.vertical, 10)
        case .email:
            Button("email") {}.padding(.vertical, 10)
        default:
            EmptyView()}}}Copy the code

By SwfitUI 3.0, TextField’s automatic avoidance was well established. You can avoid blocking the TextField being entered in different view types (such as List, Form, ScrollView), or if you use an auxiliary view or textContentType. Maybe it would have worked better if we had raised it a little bit higher, but it’s a little bit cramped right now.

Custom SubmitLabel

By default, TextField (SecureField) corresponds to return on the keyboard. By using the submitLabel modifier added in SwiftUI 3.0, We can change the return button to display text that better fits the input context.

TextField("Username", text: $username)
            .submitLabel(.next)
Copy the code

Currently supported types are:

  • continue
  • done
  • go
  • join
  • next
  • return
  • route
  • search
  • send

For example, in the previous code, we could set the corresponding display for name, email, and phoneNumber respectively:

            TextField("Name:", text: $name)
                .focused($focus, equals: .name)
                .submitLabel(.next)

            TextField("Email:", text: $email)
                .focused($focus, equals: .email)
                .submitLabel(.next)

            TextField("PhoneNumber:", text: $phoneNumber)
                .focused($focus, equals: .phone)
                .submitLabel(.return)
Copy the code

conclusion

Since SwiftUI 1.0, Apple has continued to improve TextField’s features. In version 3.0, SwiftUI not only offers more native modifiers, but also integrated management logic like FocusState and onSubmit. In 2-3 years, the native functions of SwiftUI’s primary controls will be comparable to their UIKit counterparts.

More on how to customize the TextField display will be discussed later.

Hope you found this article helpful.

The original post was posted on my blog wwww.fatbobman.com

Public number: [Swift Notepad for elbow]