I am trying to implement an inputAccessoryView on a TextField in SwiftUI. The goal is to have a "Done" Button appear above the Keyboard which when pressed gets rid of the keyboard (i.e. resignFirstResponder()).
I came across the following Medium article which purports to implement this behavior exactly as I would require, however, I am struggling to get it working.
Medium link containing method to be implemented.
I have tried to implement this in a blank XCode project, my code compiles, however, the TextField never shows up, and I cannot touch in the area it should be to bring up the keyboard. How do I correctly implement this code to get the desired behavior?
import Foundation
import UIKit
import SwiftUI
class TextFieldViewController
: UIViewController {
// our custom text field will report changes to the outside
let text: Binding<String>?
// if the toolbar (see below) is used (Done), the keyboard shall be dismissed
// and optionally we execute a provided closure
let onDismiss: (() -> Void)?
init (
text: Binding<String>
, onDismiss: (() -> Void)?) {
self.text = text
self.onDismiss = onDismiss
super.init(
nibName: nil //"<XIB>"
, bundle: nil //Bundle.main?
)
}
required init?(coder: NSCoder) {
self.text = nil
self.onDismiss = nil
super.init(coder: coder)
}
// helper function to encapsulate calling the "view" of UIViewController
fileprivate func getTextField() -> UITextField? {
return view as? UITextField
}
override func viewDidLoad() {
let textField = self.getTextField()
guard textField != nil else {
return
}
// configure a toolbar with a Done button
let toolbar = UIToolbar()
toolbar.setItems([
// just moves the Done item to the right
UIBarButtonItem(
barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace
, target: nil
, action: nil
)
, UIBarButtonItem(
title: "Done"
, style: UIBarButtonItem.Style.done
, target: self
, action: #selector(self.onSet)
)
]
, animated: true
)
toolbar.barStyle = UIBarStyle.default
toolbar.sizeToFit()
textField?.inputAccessoryView = toolbar
}
@objc private func onSet() {
let textField = self.getTextField()
textField?.resignFirstResponder()
self.text?.wrappedValue = textField?.text ?? ""
self.onDismiss?()
}
}
// The SwiftUI view, wrapping the UITextField
struct TextFieldView: View {
var text: Binding<String>
var onDismissKeyboard: (() -> Void)?
var body: some View {
TextFieldRepresentable(
text: self.text
, dismissKeyboardCallback: self.onDismissKeyboard
)
}
}
// The UIViewControllerRepresentable, feeding and controlling the UIViewController
struct TextFieldRepresentable
: UIViewControllerRepresentable {
// the callback
let dismissKeyboardCallback: (() -> Void)?
// created in the previous file/gist
let viewController: TextFieldViewController
init (
text: Binding<String>
, dismissKeyboardCallback: (() -> Void)?) {
self.dismissKeyboardCallback = dismissKeyboardCallback
self.viewController = TextFieldViewController(
text: text
, onDismiss: dismissKeyboardCallback
)
}
// UIViewControllerRepresentable
func makeUIViewController(context: Context) -> UIViewController {
return viewController
}
// UIViewControllerRepresentable
func updateUIViewController(_ viewController: UIViewController, context: Context) {
}
}
struct ContentView : View {
@State var email:String = ""
var body: some View {
HStack{
Circle()
TextFieldView(text: $email)
Circle()
}
}
}
Here is a demo with custom toolbar & binding for entered text, but simplified by excluding on dismiss callback (as it is not important for approach demo), just to have less code. Hope it will be helpful.
import SwiftUI
import UIKit
import Combine
struct CustomInputTextField : UIViewRepresentable {
@Binding var text: String
let textField = UITextField(frame: CGRect(x:0, y:0, width: 100, height: 32)) // just any
func makeUIView(context: UIViewRepresentableContext<CustomInputTextField>) -> UITextField {
return textField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomInputTextField>) {
self.textField.text = text
}
func makeCoordinator() -> CustomInputTextField.Coordinator {
let coordinator = Coordinator(self)
// configure a toolbar with a Done button
let toolbar = UIToolbar()
toolbar.setItems([
// just moves the Done item to the right
UIBarButtonItem(
barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace
, target: nil
, action: nil
)
, UIBarButtonItem(
title: "Done"
, style: UIBarButtonItem.Style.done
, target: coordinator
, action: #selector(coordinator.onSet)
)
]
, animated: true
)
toolbar.barStyle = UIBarStyle.default
toolbar.sizeToFit()
textField.inputAccessoryView = toolbar
return coordinator
}
typealias UIViewType = UITextField
class Coordinator: NSObject {
let owner: CustomInputTextField
private var subscriber: AnyCancellable
init(_ owner: CustomInputTextField) {
self.owner = owner
subscriber = NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: owner.textField)
.sink(receiveValue: { _ in
owner.$text.wrappedValue = owner.textField.text ?? ""
})
}
@objc fileprivate func onSet() {
owner.textField.resignFirstResponder()
}
}
}
struct DemoCustomKeyboardInput : View {
@State var email:String = ""
var body: some View {
VStack{
CustomInputTextField(text: $email).border(Color.black)
.padding(.horizontal)
.frame(maxHeight: 32)
Divider()
Text("Entered text: \(email)")
}
}
}
struct DemoCustomKeyboardInput_Previews: PreviewProvider {
static var previews: some View {
DemoCustomKeyboardInput()
}
}
I use this code multi line textfield.
SwiftUI
Swift5
Version 11.3 (11C29)
struct MultiLineTextField: UIViewRepresentable {
@Binding var text: String
let onEditingChanged: (Bool) -> Void
init(text: Binding<String>, onEditingChanged: @escaping (Bool) -> Void = {_ in}) {
self._text = text
self.onEditingChanged = onEditingChanged
}
func makeCoordinator() -> MultiLineTextField.Coordinator {
return MultiLineTextField.Coordinator(parent1: self)
}
func makeUIView(context: UIViewRepresentableContext<MultiLineTextField>) -> UITextView {
let textView = UITextView()
textView.isEditable = true
textView.isUserInteractionEnabled = true
textView.isScrollEnabled = true
textView.font = .systemFont(ofSize: 20)
textView.delegate = context.coordinator
textView.text = self.text
/******* toolbar add **********/
let toolbar = UIToolbar()
toolbar.setItems(
[
UIBarButtonItem(
title: "Done",
style: UIBarButtonItem.Style.done,
target: self,
action: nil
)
]
, animated: true
)
toolbar.barStyle = UIBarStyle.default
toolbar.sizeToFit()
textView.inputAccessoryView = toolbar
/******* toolbar add **********/
return textView
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<MultiLineTextField>) {
if uiView.text != self.text {
uiView.text = self.text
}
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: MultiLineTextField
let onEditingChanged: (Bool) -> Void
init(parent1: MultiLineTextField, onEditingChanged: @escaping (Bool) -> Void = {_ in}) {
self.parent = parent1
self.onEditingChanged = onEditingChanged
}
func textViewDidChange(_ textView: UITextView) {
self.parent.text = textView.text
}
func textViewDidBeginEditing(_ textView: UITextView) {
onEditingChanged(true)
}
func textViewDidEndEditing(_ textView: UITextView) {
onEditingChanged(false)
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With