如何为遵循UIKeyInput的自定义文本视图添加SwiftUI键盘工具栏
如何为遵循UIKeyInput的自定义文本视图添加SwiftUI键盘工具栏
我明白你遇到的困扰了——自定义的文本输入视图虽然实现了UIKeyInput,但SwiftUI并没有像对待原生TextField那样自动加载对应的键盘工具栏。咱们一步步调整,让它和原生控件的表现完全一致。
问题根源分析
你的代码核心问题有两个:
- 自定义视图的聚焦状态没有和SwiftUI的
FocusState正确同步,导致SwiftUI不知道该视图已经激活,对应的toolbar也就不会显示。 - 点击自定义视图时,你是手动切换
isEditingTvText,但没有更新FocusState,这就断了SwiftUI工具栏系统和自定义视图的关联。
解决方案步骤
1. 让自定义UIView同步第一响应者状态到SwiftUI
首先修改TextRenderingView,让它在成为/失去第一响应者时通知SwiftUI:
import UIKit class TextRenderingView: UIView, UIKeyInput { var input = "" // 添加回调闭包,通知SwiftUI第一响应者变化 var onBecomeFirstResponder: (() -> Void)? var onResignFirstResponder: (() -> Void)? override var canBecomeFirstResponder: Bool { true } var hasText: Bool { input.isEmpty == false } func insertText(_ text: String) { input += text setNeedsDisplay() } func deleteBackward() { _ = input.popLast() setNeedsDisplay() } override func draw(_ rect: CGRect) { let attrs: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 32)] let attributedString = NSAttributedString(string: input, attributes: attrs) attributedString.draw(in: rect) } // 重写第一响应者方法,触发回调 override func becomeFirstResponder() -> Bool { let success = super.becomeFirstResponder() if success { onBecomeFirstResponder?() } return success } override func resignFirstResponder() -> Bool { let success = super.resignFirstResponder() if success { onResignFirstResponder?() } return success } }
2. 调整UIViewRepresentable的绑定逻辑
修改TextView,让它通过Binding<Bool>接收聚焦状态,同时同步UIKit的第一响应者变化到SwiftUI:
import SwiftUI struct TextView: UIViewRepresentable { // 用Binding和SwiftUI的FocusState同步 @Binding var isFocused: Bool func makeCoordinator() -> Coordinator { Coordinator(self) } func makeUIView(context: Context) -> TextRenderingView { let vw = TextRenderingView() vw.backgroundColor = .clear // 绑定第一响应者回调到Coordinator vw.onBecomeFirstResponder = { context.coordinator.handleBecomeFirstResponder() } vw.onResignFirstResponder = { context.coordinator.handleResignFirstResponder() } return vw } func updateUIView(_ uiView: TextRenderingView, context: Context) { let isFirstResponder = uiView.isFirstResponder if isFocused && !isFirstResponder { DispatchQueue.main.async { uiView.becomeFirstResponder() } } else if !isFocused && isFirstResponder { DispatchQueue.main.async { uiView.resignFirstResponder() } } } class Coordinator: NSObject { var parent: TextView init(_ parent: TextView) { self.parent = parent } func handleBecomeFirstResponder() { parent.isFocused = true } func handleResignFirstResponder() { parent.isFocused = false } } }
3. 修正ContentView的FocusState和Toolbar逻辑
现在调整ContentView,让聚焦状态完全由FocusState驱动,并且确保每个工具栏只在对应视图激活时显示:
import SwiftUI enum LocFields { case textField case myTextView } struct ContentView: View { @State private var tfText = "" @FocusState private var focusedField: LocFields? var body: some View { VStack { TextField("Enter text", text: $tfText) .textFieldStyle(.roundedBorder) .focused($focusedField, equals: .textField) .toolbar { // 只在TextField聚焦时显示这个工具栏 if focusedField == .textField { ToolbarItemGroup(placement: .keyboard) { Button("Tap me 1!") { print("Tapped 1") } } } } // 用FocusState的投影绑定TextView的聚焦状态 TextView(isFocused: $focusedField.map { $0 == .myTextView } ?? false) .background(Color.gray.opacity(0.2)) .focused($focusedField, equals: .myTextView) .toolbar { // 只在自定义视图聚焦时显示这个工具栏 if focusedField == .myTextView { ToolbarItemGroup(placement: .keyboard) { Button("Tap me 2!") { print("Tapped 2") } // 可以添加更多按钮,比如系统的"完成"按钮 Button("Done") { focusedField = nil } .foregroundColor(.blue) } } } // 点击时直接设置FocusState,而不是手动管理isEditing .onTapGesture { focusedField = .myTextView } } .padding() } }
关键调整说明
- 聚焦状态同步:通过让
TextRenderingView在成为/失去第一响应者时通知SwiftUI,确保FocusState始终和实际输入状态一致,这样SwiftUI才会触发对应的键盘工具栏。 - Toolbar条件显示:给每个
toolbar添加if focusedField == ...的判断,避免多个工具栏叠加显示(这也是你之前看到TextField显示两个按钮的原因)。 - 统一FocusState驱动:所有输入视图的激活状态都由
FocusState管理,放弃单独的isEditing变量,让逻辑更清晰,也符合SwiftUI的设计理念。
这样调整后,你的自定义文本视图就会和原生TextField表现完全一致:激活时自动显示对应的蓝色按钮工具栏,切换焦点时工具栏也会自动切换,而且不会出现按钮叠加的问题。
内容来源于stack exchange
相关产品推荐
相关产品推荐

