Search code examples
swiftuitextfielduikeyboardtype

How can I add a .submitLabel() to a SwiftUI TextField() with a numberPad


Is it possible to add a .submitLable() to a TextField that has the keyboard type .numberPad. All the information I can find is that it's not possible but that information is 2 years old. So I hope that it might have changed.

I have the following code but the submit label doesn't appear on the keyboard when I actually run the app.

TextField("placeholder", text: $placeholder)
    .submitLabel(.next)
    .keyboardType(.numberPad)

Is there a way to add a submit label or something of similar functionality to a TextField with a keyboard type of numberPad in SwiftUI or is it just not possible with SwiftUI currently?


Solution

  • The issue is that .submitLabel() modifies the "return" key display/behavior in the keyboard, but with .numberPad there is no such key to modify. (You can see this behavior by experimenting with different values for .submitLabel() using the default keyboard type)

    It is possible to add an inputAccessoryView to a UITextField with a system defined and pre-localized .done button. (Or .next, or several others) It is somewhat cumbersome, though.

    For example, using a generic type Value that conforms to BinaryInteger:

    struct NumberTextField<Value: BinaryInteger>: UIViewRepresentable {
        @Binding var value: Value
        
        func makeUIView(context: Context) -> UITextField {
            let textField = UITextField()
            textField.keyboardType = .numberPad
            textField.delegate = context.coordinator
            textField.inputAccessoryView = createToolbar()
            return textField
        }
        
        func updateUIView(_ uiView: UITextField, context: Context) {
            uiView.text = "\(value)"
        }
        
        func makeCoordinator() -> Coordinator {
            Coordinator(value: $value)
        }
        
        private func createToolbar() -> UIToolbar {
            // if you want behavior other than "dismiss", put it in action:
            let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(UIApplication.dismissKeyboard))
            
            let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
            
            let toolbar = UIToolbar()
            toolbar.sizeToFit()
            toolbar.items = [spacer, doneButton]
    
            // you may want to check the locale for right-to-left orientation,
            // if it doesn't automatically re-orient the sequence of items.
    
            return toolbar
        }
    
        // I don't recall where I got this code, its purpose is mostly to
        // filter out non-numeric values from the input, it may be suboptimal
        class Coordinator: NSObject, UITextFieldDelegate {
            var value: Binding<Value>
            
            init(value: Binding<Value>) {
                self.value = value
            }
            
            func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
                let allowedCharacters = CharacterSet.decimalDigits
                let characterSet = CharacterSet(charactersIn: string)
                return allowedCharacters.isSuperset(of: characterSet)
            }
            
            func textFieldDidEndEditing(_ textField: UITextField) {
                // if you use a different protocol than BinaryInteger
                // you will most likely have to change this behavior
                guard let text = textField.text else { return }
                guard let integer = Int(text) else { return }
                value.wrappedValue = Value(integer)
            }
        }
    }
    

    Here, UIApplication.dismissKeyboard is an extension on UIApplication like this, which essentially tells whatever currently has focus (to show the keyboard) to give it up:

    extension UIApplication {
        @objc
        func dismissKeyboard() {
            sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
        }
    }
    

    I have used this with Swift 5.5 and a target of iOS 16.x. It should work with some prior versions of Swift / iOS, but I have not tested it with anything else.