Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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()
        }
    }
} 
like image 210
James Woodcock Avatar asked Jul 11 '26 19:07

James Woodcock


2 Answers

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()
    }
}
like image 103
Asperi Avatar answered Jul 13 '26 14:07

Asperi


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)
        }
    }
}
like image 21
sankon3sankon3 Avatar answered Jul 13 '26 16:07

sankon3sankon3