SwiftUI inputAccessoryView 实现
SwiftUI inputAccesoryView Implementation
我正在尝试在 SwiftUI 中的 TextField 上实现 inputAccessoryView。目标是让 "Done" 按钮出现在键盘上方,按下该按钮时会摆脱键盘(即 resignFirstResponder())。
我看到了以下 Medium 文章,它声称完全按照我的要求实现了这种行为,但是,我正在努力让它发挥作用。
Medium link containing method to be implemented.
我已经尝试在一个空白的 XCode 项目中实现它,我的代码可以编译,但是,TextField 从未出现,而且我无法触摸应该调出键盘的区域。我如何正确实施此代码以获得所需的行为?
代码
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()
}
}
}
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())
现在您可以隐藏和显示 "showInput" 状态的文本框。下一个问题是,您必须在特定事件中打开键盘并显示文本字段。这对 SwiftUI 来说也是不可能的,你必须回到 UiKit 并让它成为第一响应者。
总体而言,在当前状态下,无法使用键盘或特定的文本字段方法。
这是一个自定义工具栏和输入文本绑定的演示,但通过排除关闭回调进行了简化(因为它对于方法演示并不重要),只是为了减少代码。希望对你有帮助。
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()
}
}
我使用此代码多行文本字段。
SwiftUI
Swift5
版本 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)
}
}
}
我已经在 iOS 14 上使用 99% 纯 SwiftUI 解决了这个问题。
这是我的实现:
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()
}
}
我正在尝试在 SwiftUI 中的 TextField 上实现 inputAccessoryView。目标是让 "Done" 按钮出现在键盘上方,按下该按钮时会摆脱键盘(即 resignFirstResponder())。
我看到了以下 Medium 文章,它声称完全按照我的要求实现了这种行为,但是,我正在努力让它发挥作用。
Medium link containing method to be implemented.
我已经尝试在一个空白的 XCode 项目中实现它,我的代码可以编译,但是,TextField 从未出现,而且我无法触摸应该调出键盘的区域。我如何正确实施此代码以获得所需的行为?
代码
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()
}
}
}
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())
现在您可以隐藏和显示 "showInput" 状态的文本框。下一个问题是,您必须在特定事件中打开键盘并显示文本字段。这对 SwiftUI 来说也是不可能的,你必须回到 UiKit 并让它成为第一响应者。 总体而言,在当前状态下,无法使用键盘或特定的文本字段方法。
这是一个自定义工具栏和输入文本绑定的演示,但通过排除关闭回调进行了简化(因为它对于方法演示并不重要),只是为了减少代码。希望对你有帮助。
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()
}
}
我使用此代码多行文本字段。
SwiftUI
Swift5
版本 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)
}
}
}
我已经在 iOS 14 上使用 99% 纯 SwiftUI 解决了这个问题。 这是我的实现:
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()
}
}