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
struct InputAccessory: UIViewRepresentable {
var placeHolder: String
func makeUIView(context: Context) -> UITextField {
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: nil
)
]
, animated: true
)
toolbar.barStyle = UIBarStyle.default
toolbar.sizeToFit()
let customView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 44))
customView.backgroundColor = UIColor.red
let sampleTextField = UITextField(frame: CGRect(x: 20, y: 100, width: 300, height: 40))
sampleTextField.inputAccessoryView = toolbar
sampleTextField.placeholder = placeHolder
return sampleTextField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.endEditing(true)
}
}
struct ContentView : View {
@State var email:String = "e"
var body: some View {
HStack{
Circle()
InputAccessory(placeHolder: "hello")
Circle()
}
}
}
PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())
Now you can hide and show the textfield with the "showInput" state. The next problem is, that you have to open your keyboard at a certain event and show the textfield. That's again not possible with SwiftUI and you have to go back to UiKit and making it first responder. Overall, at the current state it's not possible to work with the keyboard or with the certain textfield method.
I've solved this problem using 99% pure SwiftUI on iOS 14. That's my implementation:
import SwiftUI
struct ContentView: View {
@State private var showtextFieldToolbar = false
@State private var text = ""
var body: some View {
ZStack {
VStack {
TextField("Write here", text: $text) { isChanged in
if isChanged {
showtextFieldToolbar = true
}
} onCommit: {
showtextFieldToolbar = false
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
VStack {
Spacer()
if showtextFieldToolbar {
HStack {
Spacer()
Button("Close") {
showtextFieldToolbar = false
UIApplication.shared
.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
.foregroundColor(Color.black)
.padding(.trailing, 12)
}
.frame(idealWidth: .infinity, maxWidth: .infinity,
idealHeight: 44, maxHeight: 44,
alignment: .center)
.background(Color.gray)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
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)
}
}
}