22
votes

I'm trying to find a way to add a key or button to the SwiftUI numberPad. The only references I have found say it is not possible. In the Swift world I added a toolbar with a button to dismiss the keyboard or perform some other function.

I would even build a ZStack view with the button on top but I can't find a way to add the numberPad to my own view.

All I'm really trying to do in this case is dismiss the numberPad when the data is entered. I first attempted to modify the SceneDelegate to dismiss on taps, but that only works if I tap in another text or textfield view not in open space on the view.

window.rootViewController = UIHostingController(rootView: contentView.onTapGesture {
    window.endEditing(true)
})

Ideally, I'd add a Done key to the lower left space. Second best add a toolbar if it can be done in SwiftUI.

enter image description here

Any guidance would be appreciated.

Xcode Version 11.2.1 (11B500)

4

4 Answers

23
votes

Solved the issue using UIRepresentable protocol

struct TestTextfield: UIViewRepresentable {
    @Binding var text: String
    var keyType: UIKeyboardType
    func makeUIView(context: Context) -> UITextField {
        let textfield = UITextField()
      textfield.keyboardType = keyType
        let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: textfield.frame.size.width, height: 44))
        let doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(textfield.doneButtonTapped(button:)))
        toolBar.items = [doneButton]
        toolBar.setItems([doneButton], animated: true)
        textfield.inputAccessoryView = toolBar
        return textfield
    }
    
    func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.text = text
        
    }
}

extension  UITextField{
    @objc func doneButtonTapped(button:UIBarButtonItem) -> Void {
       self.resignFirstResponder()
    }

}

Using in content view

struct ContentView : View {
    @State var text = ""
    
    var body: some View {
        TestTextfield(text: $text, keyType: UIKeyboardType.phonePad)
            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 50)
            .overlay(
                RoundedRectangle(cornerRadius: 16)
                    .stroke(Color.blue, lineWidth: 4)
        )
    }
}
14
votes

I did it in 5 lines using the SwiftUI-Introspect library! I had a problem that the Representable Text Field did not react in any way to padding and frame. Everything was decided using this library!

Link: https://github.com/siteline/SwiftUI-Introspect

import SwiftUI
import Introspect

struct ContentView : View {
@State var text = ""

var body: some View {
    TextField("placeHolder", text: $text)
       .keyboardType(.default)
       .introspectTextField { (textField) in
           let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: textField.frame.size.width, height: 44))
           let flexButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: nil, action: nil)
           let doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(textField.doneButtonTapped(button:)))
           doneButton.tintColor = .systemPink
           toolBar.items = [flexButton, doneButton]
           toolBar.setItems([flexButton, doneButton], animated: true)
           textField.inputAccessoryView = toolBar
        }
}

extension  UITextField {
   @objc func doneButtonTapped(button:UIBarButtonItem) -> Void {
      self.resignFirstResponder()
   }
}

output

7
votes

If there is an issue with @Binding implement the Coordinator class which conforms to the UITextFieldDelegate. This will allow one to be able to customize the TextField further if required.

struct DecimalTextField: UIViewRepresentable {
     private var placeholder: String
     @Binding var text: String

     init(_ placeholder: String, text: Binding<String>) {
        self.placeholder = placeholder
        self._text = text
     } 

     func makeUIView(context: Context) -> UITextField {
        let textfield = UITextField()
        textfield.keyboardType = .decimalPad
        textfield.delegate = context.coordinator
        textfield.placeholder = placeholder
        let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: textfield.frame.size.width, height: 44))
        let doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(textfield.doneButtonTapped(button:)))
        toolBar.items = [doneButton]
        toolBar.setItems([doneButton], animated: true)
        textfield.inputAccessoryView = toolBar
        return textfield
     }

     func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.text = text
     }

     func makeCoordinator() -> Coordinator {
        Coordinator(self)
     }

     class Coordinator: NSObject, UITextFieldDelegate {
        var parent: DecimalTextField
    
     init(_ textField: DecimalTextField) {
        self.parent = textField
     }
    
     func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
         if let currentValue = textField.text as NSString? {
             let proposedValue = currentValue.replacingCharacters(in: range, with: string) as String
             self.parent.text = proposedValue
         }
         return true
     }
   }
 }
3
votes

iOS 15

Starting from iOS 15 we can add custom views right above the keyboard using the new keyboard ToolbarItemPlacement:

Form {
    ...
}
.toolbar {
    ToolbarItem(placement: .keyboard) {
        Button("Do something") {
            print("something")
        }
    }
}

You can also combine this with the new @FocusState property wrapper to remove focus from the currently edited element:

@FocusState private var isFocused: Bool
...

var body: some View {
    Form {
        TextField(...)
            .focused($isFocused)
    }
    .toolbar {
        ToolbarItem(placement: .keyboard) {
            Button("Done") {
                isFocused = false
            }
        }
    }
}