Is there any way to set inputView for TextField in SwiftUI?
If you want to have a TextField
and choose its text using a Picker
in SwiftUI
. And you don't want to integrate UIKit
in SwiftUI
, the bellow solution may give you some ideas:
import SwiftUI
struct ContentView: View {
@State private var selection = 0
@State private var textfieldValue = ""
@State private var textfieldValue2 = ""
@State private var ispickershowing = false
var values = ["V1", "V2", "V3"]
var body: some View {
VStack {
TextField("Pick one from the picker:", text: $textfieldValue, onEditingChanged: {
edit in
if edit {
self.ispickershowing = true
} else {
self.ispickershowing = false
}
})
if ispickershowing {
Picker(selection: $selection, label:
Text("Pick one:")
, content: {
ForEach(0 ..< values.count) { index in
Text(self.values[index])
.tag(index)
}
})
Text("you have picked \(self.values[self.selection])")
Button(action: {
self.textfieldValue = self.values[self.selection]
}, label: {
Text("Done")
})
}
TextField("simple textField", text: $textfieldValue2)
}
}
}
This is a textfield that can have an input view that is either a picker, a datepicker, or a keyboard:
import Foundation
import SwiftUI
struct CTextField: UIViewRepresentable {
enum PickerType {
case keyboard(type: UIKeyboardType, autocapitalization: UITextAutocapitalizationType, autocorrection: UITextAutocorrectionType)
case datePicker(minDate: Date, maxDate: Date)
case customList(list: [String])
}
var pickerType: CTextField.PickerType
@Binding var text: String {
didSet{
print("text aha: ", text)
}
}
let placeholder: String
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
textField.placeholder = placeholder
textField.frame.size.height = 36
textField.borderStyle = .roundedRect
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
if !self.text.isEmpty{
textField.text = self.text
}
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
switch pickerType {
case .datePicker:
uiView.text = self.text
case .customList:
uiView.text = self.text
default:
break
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
final class Coordinator: NSObject {
var parent: CTextField
init(_ parent: CTextField) {
self.parent = parent
}
private func setPickerType(textField: UITextField) {
switch parent.pickerType {
case .keyboard(let type, let autocapitalization, let autocorrection):
textField.keyboardType = type
textField.inputView = nil
textField.autocapitalizationType = autocapitalization
textField.autocorrectionType = autocorrection
case .customList(let list):
textField.inputView = getPicker()
let row = list.firstIndex(of: parent.text)
let myPicker = textField.inputView as! UIPickerView
myPicker.selectRow(row!, inComponent: 0, animated: true)
case .datePicker(let minDate, let maxDate):
textField.inputView = getDatePicker(minDate: minDate, maxDate: maxDate)
}
textField.inputAccessoryView = getToolBar()
}
private func getPicker() -> UIPickerView {
let picker = UIPickerView()
picker.backgroundColor = UIColor.systemBackground
picker.delegate = self
picker.dataSource = self
return picker
}
private func getDatePicker(minDate: Date, maxDate: Date) -> UIDatePicker {
let picker = UIDatePicker()
picker.datePickerMode = .date
picker.backgroundColor = UIColor.systemBackground
picker.maximumDate = maxDate
picker.minimumDate = minDate
picker.addTarget(self, action: #selector(handleDatePicker(sender:)), for: .valueChanged)
return picker
}
@objc func handleDatePicker(sender: UIDatePicker) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd MMM yyyy"
parent.text = dateFormatter.string(from: sender.date)
}
private func getToolBar() -> UIToolbar {
let toolBar = UIToolbar()
toolBar.barStyle = UIBarStyle.default
toolBar.backgroundColor = UIColor.systemBackground
toolBar.isTranslucent = true
toolBar.sizeToFit()
let spaceButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: nil, action: nil)
let doneButton = UIBarButtonItem(title: "Done", style: UIBarButtonItem.Style.done, target: self, action: #selector(self.donePicker))
toolBar.setItems([spaceButton, doneButton], animated: false)
toolBar.isUserInteractionEnabled = true
return toolBar
}
@objc func donePicker() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
}
extension CTextField.Coordinator: UIPickerViewDataSource{
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
switch parent.pickerType {
case .customList(let list):
return list.count
default:
return 0
}
}
}
extension CTextField.Coordinator: UIPickerViewDelegate {
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
switch parent.pickerType {
case .customList(let list):
return list[row]
default:
return ""
}
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
switch parent.pickerType {
case .customList(let list):
parent.text = list[row]
print("parent.text is now: ", parent.text)
default:
break
}
}
}
extension CTextField.Coordinator: UITextFieldDelegate {
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
setPickerType(textField: textField)
return true
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
defer {
if let currentText = textField.text, let stringRange = Range(range, in: currentText) {
parent.text = currentText.replacingCharacters(in: stringRange, with: string)
}
}
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
donePicker()
}
}
Assuming you have a ViewModel like this:
import SwiftUI
import Combine
let FRIENDS = ["Raquel", "Shekinah", "Sedh", "Sophia"]
class UserSettingsVM: ObservableObject {
@Published var name = FRIENDS[0]
@Published var greet = ""
}
You can use it like this:
import SwiftUI
struct FriendsView: View {
@ObservedObject var vm = UserSettingsVM()
var body: some View {
ScrollView {
VStack {
Group {
Text(vm.name)
.padding()
Text(vm.greet)
.padding()
CTextField(pickerType: .customList(list: FRIENDS), text: $vm.name, placeholder: "Required")
CTextField(pickerType: .keyboard(type: .default, autocapitalization: .none, autocorrection: .no
), text: $vm.greet, placeholder: "Required")
}
.padding()
}
}
}
}
As of Xcode 11.4, SwiftUI's TextField
does not have an equivalent of the inputView
property of UITextField
.
You can work around it by bridging a UIKit UITextField
to SwiftUI, and by bridging a SwiftUI Picker
to UIKit. You'll need to set the text field's inputViewController
property rather than its inputView
property.
To bridge a UITextField
to SwiftUI
Use UIViewRepresentable
to wrap the UITextField
in a SwiftUI View
. Since you create the UITextField
, you can set its inputViewController
property to a UIViewController
that you create.
To bridge a SwiftUI Picker
into UIKit
UseUIHostingController
to wrap a SwiftUI Picker
in a UIViewController
. Set the text field's inputViewController
to your UIHostingController
instance.
The only issue which I found using the above-mentioned solution was that whenever the keyboard gets into the editing phase, then the picker was presented and along with it the keyboard also gets presented.
So there was no way to hide the keyboard and present the picker. Therefore I have written a custom struct to handle this behaviour similar to what we do using UITextField inputView. You can use it. This works for my use case.
You can also customise the picker, as well as textfield in the makeUIView methods like I, have done with the background colour of the picker.
struct TextFieldWithPickerAsInputView : UIViewRepresentable {
var data : [String]
var placeholder : String
@Binding var selectionIndex : Int
@Binding var text : String?
private let textField = UITextField()
private let picker = UIPickerView()
func makeCoordinator() -> TextFieldWithPickerAsInputView.Coordinator {
Coordinator(textfield: self)
}
func makeUIView(context: UIViewRepresentableContext<TextFieldWithPickerAsInputView>) -> UITextField {
picker.delegate = context.coordinator
picker.dataSource = context.coordinator
picker.backgroundColor = .yellow
picker.tintColor = .black
textField.placeholder = placeholder
textField.inputView = picker
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<TextFieldWithPickerAsInputView>) {
uiView.text = text
}
class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate , UITextFieldDelegate {
private let parent : TextFieldWithPickerAsInputView
init(textfield : TextFieldWithPickerAsInputView) {
self.parent = textfield
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return self.parent.data.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return self.parent.data[row]
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
self.parent.$selectionIndex.wrappedValue = row
self.parent.text = self.parent.data[self.parent.selectionIndex]
self.parent.textField.endEditing(true)
}
func textFieldDidEndEditing(_ textField: UITextField) {
self.parent.textField.resignFirstResponder()
}
}
}
You can use this as:-
struct ContentView : View {
@State var gender : String? = nil
@State var arrGenders = ["Male","Female","Unknown"]
@State var selectionIndex = 0
var body : some View {
VStack {
TextFieldWithPickerAsInputView(data: self.arrGenders, placeholder: "select your gender", selectionIndex: self.$selectionIndex, text: self.$gender)
}
}
}