29

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 Answers 4

28

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)
        )
    }
}
Sign up to request clarification or add additional context in comments.

7 Comments

I was hoping for a pure SwiftUI solution - but I think you are correct - there isn't one.
It's better to implement a coordinator instanced by makeCoordinator() and use that for delegation and selector target. I agree with the other comments, there doesn't seem to exist a pure SwiftUI solution yet.
When attempting to implement the above code the @Binding text value does not get passed when Done is selected to the original @State text value. What am I missing?
value is not passed on done button click, check binding.
This solution doesn't support the SwiftUI TextField Label property, so it doesn't fully work in a SwiftUI Form.
|
16

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
            }
        }
    }
}

3 Comments

Do you have an idea how to make the toolbar visible permanently, i.e. even if the TextField has no focus or after keyboard is dismissed?
You can use another placement for this, e.g. bottomBar.
bottomBar is completely different and cannot be used for this.
15

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

2 Comments

Thanks. I'm not a big fan of 3rd party frameworks, I've been burned too many times when an Xcode update breaks them and I end up recoding it myself. But hopefully your research will help someone else!
I would normally share JohnSF's avoidance of 3rd party frameworks but now that I've looked into Introspect it is a fantastic concept that Apple would be wise to integrate. Ultimately, it may be redundant but until SwiftUI reaches that level of maturity, Introspect will be a huge help.
9

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
     }
   }
 }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.