iOS:用SwiftUI按钮控制UITextField并隐藏系统键盘
解决方案:SwiftUI自定义按键面板绑定UITextField(隐藏系统键盘)
核心问题分析
原方案通过将KeypadView包装成UIHostingController作为UITextField的inputView,在iPad上会因键盘容器的布局特性导致显示异常;同时直接持有UITextField实例的方式,在脱离inputView场景下会出现引用失效问题。我们需要改为SwiftUI绑定驱动的方式,彻底摆脱对UIKit键盘容器的依赖,同时稳定隐藏系统键盘。
步骤1:重构WrappedTextField(移除inputView依赖,暴露必要绑定)
修改WrappedTextField,去掉和WrappedTextFieldViewController相关的代码,直接禁用系统键盘,并添加光标范围绑定,让SwiftUI层能控制和同步光标位置:
struct WrappedTextField: UIViewRepresentable { final class ViewModel { var placeholder: String var text: Binding<String> var selectedRange: Binding<NSRange>? var font: UIFont? var textColor: UIColor? init(_ placeholder: String, _ text: Binding<String>) { self.placeholder = placeholder self.text = text } } private var model: ViewModel init(_ placeholder: String, text: Binding<String>) { model = ViewModel(placeholder, text) } // MARK: Modifiers func font(_ font: UIFont) -> WrappedTextField { model.font = font return self } func textColor(_ textColor: Color) -> WrappedTextField { model.textColor = UIColor(textColor) return self } func selectedRange(_ range: Binding<NSRange>) -> WrappedTextField { model.selectedRange = range return self } // MARK: Lifecycle Methods func makeUIView(context: Context) -> UITextField { let textField = UITextField() // 彻底隐藏系统键盘 textField.inputView = UIView(frame: .zero) textField.autocapitalizationType = .allCharacters textField.autocorrectionType = .no textField.textAlignment = .right textField.delegate = context.coordinator return textField } func updateUIView(_ uiView: UITextField, context: Context) { uiView.placeholder = model.placeholder // 仅当文本不同步时更新,避免光标位置被覆盖 if uiView.text != model.text.wrappedValue { uiView.text = model.text.wrappedValue } uiView.font = model.font uiView.textColor = model.textColor uiView.setContentHuggingPriority(.defaultHigh, for: .vertical) uiView.setContentHuggingPriority(.defaultLow, for: .horizontal) // 同步光标位置到文本框 if let range = model.selectedRange?.wrappedValue, let start = uiView.position(from: uiView.beginningOfDocument, offset: range.location), let end = uiView.position(from: start, offset: range.length) { uiView.selectedTextRange = uiView.textRange(from: start, to: end) } } func makeCoordinator() -> Coordinator { Coordinator(self) } } extension WrappedTextField { final class Coordinator: NSObject, UITextFieldDelegate { var parent: WrappedTextField init(_ parent: WrappedTextField) { self.parent = parent } func textFieldDidChangeSelection(_ textField: UITextField) { // 同步文本到SwiftUI绑定 if let text = textField.text { parent.model.text.wrappedValue = text } // 同步光标范围到SwiftUI绑定 if let selectedRange = textField.selectedTextRange { let location = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start) let length = textField.offset(from: selectedRange.start, to: selectedRange.end) parent.model.selectedRange?.wrappedValue = NSRange(location: location, length: length) } } } }
步骤2:重构KeypadView(用SwiftUI绑定替代UITextField引用)
修改KeypadView,不再持有UITextField实例,通过文本绑定、光标范围绑定处理输入逻辑,完全适配SwiftUI响应式机制:
struct KeypadView: View { @Binding var text: String @Binding var selectedRange: NSRange @State private var isDeleteDisabled: Bool init(text: Binding<String>, selectedRange: Binding<NSRange>) { self._text = text self._selectedRange = selectedRange self._isDeleteDisabled = State(initialValue: text.wrappedValue.isEmpty) } var body: some View { HStack(spacing: 8) { VStack(spacing: 8) { Button("N") { insertCharacter("N") } Button("S") { insertCharacter("S") } Button("E") { insertCharacter("E") } Button("W") { insertCharacter("W") } } VStack(spacing: 8) { Button("1") { insertCharacter("1") } Button("4") { insertCharacter("4") } Button("7") { insertCharacter("7") } Button(".") { insertCharacter(".") } } VStack(spacing: 8) { Button("2") { insertCharacter("2") } Button("5") { insertCharacter("5") } Button("8") { insertCharacter("8") } Button("0") { insertCharacter("0") } } VStack(spacing: 8) { Button("3") { insertCharacter("3") } Button("6") { insertCharacter("6") } Button("9") { insertCharacter("9") } Button("Del") { deleteBackward() } .disabled(isDeleteDisabled) } } .onChange(of: text) { newValue in isDeleteDisabled = newValue.isEmpty } } private func insertCharacter(_ char: String) { let start = selectedRange.location let end = start + selectedRange.length // 插入字符到光标位置 let newText = text.prefix(start) + char + text.suffix(text.count - end) text = newText // 更新光标到插入字符之后 selectedRange = NSRange(location: start + 1, length: 0) } private func deleteBackward() { guard !text.isEmpty else { return } let start = selectedRange.location let end = start + selectedRange.length if end > start { // 删除选中的文本 text = text.prefix(start) + text.suffix(text.count - end) selectedRange = NSRange(location: start, length: 0) } else if start > 0 { // 删除光标前的字符 text = text.prefix(start - 1) + text.suffix(text.count - start) selectedRange = NSRange(location: start - 1, length: 0) } } }
步骤3:修改ContentView(绑定文本和光标范围)
在ContentView中创建文本和光标范围的状态变量,分别绑定到WrappedTextField和KeypadView:
struct ContentView: View { @State private var inputText: String = "" @State private var cursorRange: NSRange = NSRange(location: 0, length: 0) var body: some View { VStack(spacing: 20) { HStack { Text("Label Here:") WrappedTextField("Press Buttons", text: $inputText) .selectedRange($cursorRange) .frame(height: 44) .border(.gray) } KeypadView(text: $inputText, selectedRange: $cursorRange) .padding() .background(.gray.opacity(0.1)) } .padding() } }
关键改动说明
- 移除UIHostingController依赖:不再将KeypadView作为inputView嵌入,直接放在SwiftUI布局中,彻底解决iPad适配问题。
- 绑定驱动交互:通过
@Binding同步文本和光标位置,KeypadView完全通过SwiftUI状态操作文本,不再依赖UITextField实例。 - 稳定隐藏系统键盘:设置
inputView = UIView(frame: .zero)彻底隐藏系统键盘,同时保留文本框的编辑状态。 - 光标位置双向同步:文本框的光标变化会同步到SwiftUI状态,KeypadView操作后也会更新光标位置,保持和文本框的一致性。
内容的提问来源于stack exchange,提问作者David H.
相关产品推荐
相关产品推荐

