You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为遵循UIKeyInput的自定义文本视图添加SwiftUI键盘工具栏

如何为遵循UIKeyInput的自定义文本视图添加SwiftUI键盘工具栏

我明白你遇到的困扰了——自定义的文本输入视图虽然实现了UIKeyInput,但SwiftUI并没有像对待原生TextField那样自动加载对应的键盘工具栏。咱们一步步调整,让它和原生控件的表现完全一致。

问题根源分析

你的代码核心问题有两个:

  1. 自定义视图的聚焦状态没有和SwiftUI的FocusState正确同步,导致SwiftUI不知道该视图已经激活,对应的toolbar也就不会显示。
  2. 点击自定义视图时,你是手动切换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()
    }
}

关键调整说明

  1. 聚焦状态同步:通过让TextRenderingView在成为/失去第一响应者时通知SwiftUI,确保FocusState始终和实际输入状态一致,这样SwiftUI才会触发对应的键盘工具栏。
  2. Toolbar条件显示:给每个toolbar添加if focusedField == ...的判断,避免多个工具栏叠加显示(这也是你之前看到TextField显示两个按钮的原因)。
  3. 统一FocusState驱动:所有输入视图的激活状态都由FocusState管理,放弃单独的isEditing变量,让逻辑更清晰,也符合SwiftUI的设计理念。

这样调整后,你的自定义文本视图就会和原生TextField表现完全一致:激活时自动显示对应的蓝色按钮工具栏,切换焦点时工具栏也会自动切换,而且不会出现按钮叠加的问题。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 11:18:07