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

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

关键改动说明

  1. 移除UIHostingController依赖:不再将KeypadView作为inputView嵌入,直接放在SwiftUI布局中,彻底解决iPad适配问题。
  2. 绑定驱动交互:通过@Binding同步文本和光标位置,KeypadView完全通过SwiftUI状态操作文本,不再依赖UITextField实例。
  3. 稳定隐藏系统键盘:设置inputView = UIView(frame: .zero)彻底隐藏系统键盘,同时保留文本框的编辑状态。
  4. 光标位置双向同步:文本框的光标变化会同步到SwiftUI状态,KeypadView操作后也会更新光标位置,保持和文本框的一致性。

内容的提问来源于stack exchange,提问作者David H.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 20:53:11