'SwiftUI inputAccesoryView Implementation

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?

Code

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


Solution 1:[1]

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

backup

Solution 2:[2]

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

Solution 3:[3]

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

Solution 4:[4]

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.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 sankon3sankon3
Solution 3 DungeonDev
Solution 4 fakiho