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

669 Views Asked by At

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?

2

There are 2 best solutions below

1
Jason G On BEST ANSWER

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.

0
Hong On

Here is my method:

  1. Use enum for FocusState.

  2. Add focused value to each textfield.

  3. Create a toolbaritem button on the keyboard with action.

  4. Check the current focused value and pass it into a function.

  5. Assign the next focused value into current focusState.

    enum FocusText {
        case apple, orange
    }
    @State private var apple: String = ""
    @State private var orange: String = ""
    @FocusState private var focusField: FocusText?
    
    TextField("Type here", value: $apple)
       .focused($focusField, equals:.apple)
       .keyboardType(.decimalPad)
    
    TextField("Type here", value: $orange)
       .focused($focusField, equals:.orange)
       .keyboardType(.decimalPad)
    
    ToolbarItem(placement: .keyboard) {
    
    Button("Next")
    {  UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),to: nil, from: nil, for: nil)                                
    
    if focusField == .apple {
    focusField = .orange
      }
    }
    

If you want it to go to the next column instead of a dedicated one:

  1. Make FocusText iterable

  2. Read the currentValue of focusedState

  3. pass it into a func to get the next case

  4. assign it to current focusedState (Make sure the cases are in correct order)

     enum FocusText: CaseIterable {
     case apple, orange
     }
    
     Button("Next")
         { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),to: nil, from: nil, for: nil)                                
    
     var currentField = focusField.unsafelyUnwrapped
     focusField = nextCase(of: currentField)
     }
    
     func nextCase(of focusField: FocusText) -> FocusText {
     let allCases = FocusText.allCases
     let currentIndex = allCases.firstIndex(of: focusField)!
     let nextIndex = allCases.index(after: currentIndex)
     return allCases[nextIndex % allCases.count] 
     }