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

Tags:

Swift

Swiftui