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]