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

SwiftUI开发:打造支持多输入类型的可复用自定义TextField

优化SwiftUI多类型文本输入的实现方案

你的这种基于泛型+自定义格式化器协议的封装思路本身是非常合理的,能够很好地实现不同输入类型的复用和解耦。下面是一些可以优化的方向和更完善的实现细节:

一、优化内部状态与编辑流程

  1. 修正状态声明方式:你当前代码里的text: State<String>写法不符合SwiftUI规范,应该使用@State属性包装器管理内部显示文本,初始化时从绑定的value同步初始显示内容。
  2. 精准监听焦点状态:使用@FocusState替代单纯的onChange监听输入框焦点变化,能更可靠地触发失焦后的最终值同步逻辑,避免不必要的重复更新。
  3. 区分显示文本与实际值:编辑过程中维护格式化后的显示文本,失焦后再将解析后的纯值同步到绑定变量,同时处理无效输入的回滚逻辑(比如输入不合法时重置显示文本为当前有效值)。

二、增强格式化器协议的能力

给FormatterProtocol补充更多必要方法,让它不仅能做格式转换,还能处理输入校验和内容过滤:

protocol FormatterProtocol {
    associatedtype Input
    // 将实际值转换为显示文本
    func string(from value: Input) -> String?
    // 将显示文本解析为实际值
    func value(from string: String) -> Input?
    // 验证输入是否合法
    func isValid(_ string: String) -> Bool
    // 过滤非法输入字符、限制输入长度
    func sanitizeInput(_ string: String) -> String
}

三、完善CoreTextField的实现

结合上面的优化点,改进后的CoreTextField实现如下:

struct CoreTextField<Value, InputFormatter: FormatterProtocol>: View where InputFormatter.Input == Value {
    @Binding private var value: Value
    @State private var displayText: String
    private let formatter: InputFormatter
    @FocusState private var isFocused: Bool

    init(value: Binding<Value>, formatter: InputFormatter) {
        self._value = value
        self.formatter = formatter
        self._displayText = State(initialValue: formatter.string(from: value.wrappedValue) ?? "")
    }

    var body: some View {
        TextField("请输入", text: $displayText)
            .focused($isFocused)
            // 实时过滤非法字符并应用格式
            .onChange(of: displayText) { newText in
                let sanitizedText = formatter.sanitizeInput(newText)
                if sanitizedText != displayText {
                    displayText = sanitizedText
                }
            }
            // 失焦时同步实际值
            .onChange(of: isFocused) { isFocused in
                guard !isFocused else { return }
                if let parsedValue = formatter.value(from: displayText) {
                    value = parsedValue
                } else {
                    // 无效输入时回滚显示文本
                    displayText = formatter.string(from: value) ?? ""
                }
            }
            // 实时显示合法性提示
            .overlay(alignment: .trailing) {
                if !formatter.isValid(displayText) {
                    Image(systemName: "exclamationmark.circle")
                        .foregroundColor(.red)
                        .padding(.trailing, 8)
                }
            }
    }
}

四、便捷封装与使用

为提升开发效率,可以针对常用输入类型封装快捷初始化方法或专属TextField类型:

// 示例:手机号输入框
struct PhoneNumberTextField: View {
    @Binding var phoneNumber: String
    var body: some View {
        CoreTextField(value: $phoneNumber, formatter: PhoneNumberFormatter())
            .keyboardType(.phonePad)
    }
}

// 手机号格式化器实现
struct PhoneNumberFormatter: FormatterProtocol {
    typealias Input = String

    func string(from value: String) -> String? {
        let digits = value.filter(\.isNumber)
        guard digits.count == 11 else { return value }
        let parts = [digits.prefix(3), digits.dropFirst(3).prefix(4), digits.dropFirst(7)]
        return parts.map(String.init).joined(separator: " ")
    }

    func value(from string: String) -> String? {
        let digits = string.filter(\.isNumber)
        return digits.count == 11 ? digits : nil
    }

    func isValid(_ string: String) -> Bool {
        string.filter(\.isNumber).count == 11
    }

    func sanitizeInput(_ string: String) -> String {
        let digits = string.filter(\.isNumber).prefix(11)
        return string(from: String(digits)) ?? String(digits)
    }
}

// 使用示例
struct ContentView: View {
    @State private var phone = ""
    @State private var amount = 0.0

    var body: some View {
        VStack(spacing: 16) {
            PhoneNumberTextField(phoneNumber: $phone)
                .padding()
            CoreTextField(value: $amount, formatter: CurrencyFormatter())
                .padding()
                .keyboardType(.decimalPad)
        }
    }
}

五、可选的替代思路

如果你的场景不需要极度自定义的格式,可以尝试结合SwiftUI原生的TextField(formatter:)API,基于系统Formatter(如NumberFormatter)扩展自定义逻辑,这样能复用系统自带的键盘适配、基础输入验证等行为。但对于手机号、身份证号这类高度自定义的输入类型,你的泛型+自定义协议方案依然是更灵活的选择。

内容的提问来源于stack exchange,提问作者cakiz

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 15:45:00