SwiftUI inputAccesoryView Implementation

余生颓废 提交于 2020-06-11 11:47:11

问题


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

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



回答2:


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.




回答3:


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


来源:https://stackoverflow.com/questions/59114647/swiftui-inputaccesoryview-implementation

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!