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

SwiftUI实现DD MM YYYY格式出生日期输入的技术问询

问题描述

我正在构建一个注册界面,要求用户以DD MM YYYY格式输入出生日期。目前我使用单个枚举实现了8个TextField的输入逻辑,代码如下:

struct Test: View {
    @StateObject var testViewModel: TestViewModel = .init()
    @FocusState var activeField: BirthdayFields?
    var body: some View {
        ZStack {
            Color.black.ignoresSafeArea()
            VStack {
                Spacer()
                BirthdayField()
                    .padding()
                Button{Task {await testViewModel.letsDate()}}
                       label: {
                    Text("Birthday")
                        .keyboardType(.numberPad)
                        .padding()
                        .frame(maxWidth: .infinity)
                        .background{
                            RoundedRectangle(cornerRadius: 10, style: .continuous)
                                .fill(.yellow)
                        }
                }
                .disabled(checkStates())
                .opacity(checkStates() ? 0.4 : 1)
                Spacer()
            }
            .onChange(of: testViewModel.dobFields) { newValue in
               DOBConditions(value: newValue)
            }
        }
    }
    @ViewBuilder
    func BirthdayField() -> some View {
        HStack(spacing: 6) {
            ForEach(0..<8, id: \.self) { index in
                VStack {
                    TextField("D", text: $testViewModel.dobFields[index])
                        .foregroundColor(.gray)
                        .keyboardType(.numberPad)
                        .textContentType(.dateTime)
                        .multilineTextAlignment(.center)
                        .focused($activeField, equals: activeStateForIndex(index: index))
                        .onAppear{
                            DispatchQueue.main.async {
                                activeField = activeStateForIndex(index: index)
                            }
                        }
                    Rectangle()
                        .fill(activeField == activeStateForIndex(index: index) ? .blue : .gray.opacity(0.4))
                        .frame( height: 4)
                }
            }
        }
    }
    func checkStates() -> Bool {
        for index in 0..<8{
            if testViewModel.dobFields[index].isEmpty{return true}
        }
        return false
    }
    func activeStateForIndex(index: Int) -> BirthdayFields {
        switch index {
        case 0: return .field1
        case 1: return .field2
        case 2: return .field3
        case 3: return .field4
        case 4: return .field5
        case 5: return .field6
        case 6: return .field7
        default: return .field8
        }
    }
    enum BirthdayFields {
        case field1
        case field2
        case field3
        case field4
        case field5
        case field6
        case field7
        case field8
    }
    func DOBConditions(value: [String]) {
        for index in 0..<8{
            if value[index].count == 8{
                DispatchQueue.main.async {
                    testViewModel.dobText = value[index]
                    testViewModel.dobFields[index] = ""
                }
                return
            }
        }
        // moving next field if current field type
        for index in 0..<7{
            if value[index].count == 1 && activeStateForIndex(index: index) == activeField {
                activeField = activeStateForIndex(index: index + 1)
            }
        }
        // moving back if current is empty and previous is not empty
        for index in 1...7{
            if value[index].isEmpty && !value[index - 1].isEmpty{
                activeField = activeStateForIndex(index: index - 1)
            }
        }
        // limiting only one text
        for index in 0..<8{
            if value[index].count > 1{
                testViewModel.dobFields[index] = String(value[index].last!)
            }
        }
    }
}

当前实现的效果未达到DD MM YYYY格式的要求,请问我该如何实现该格式?是否需要为日期、月份、年份创建单独的枚举?字段切换及删除操作的逻辑又该如何处理?


解决方案

1. 结构优化:无需单独枚举,但要优化状态可读性

不用拆分枚举,直接把原枚举的命名改成对应输入位的含义,让后续逻辑更清晰:

enum BirthdayFields {
    case day1, day2, month1, month2, year1, year2, year3, year4
}

同时可以把8个输入框按**日(2位)、月(2位)、年(4位)**分组,加入视觉分隔符,用户一眼就能看懂输入格式。

2. 实现DD MM YYYY格式校验

核心要做输入位的规则限制:

  • 日第一位只能是0/1/2/3(因为日最大31),如果第一位是3,第二位只能是0/1
  • 月份第一位只能是0/1,第一位是1时,第二位只能是0/1/2
  • 年份可根据需求限制范围(比如1900到当前年份)

3. 字段切换与删除逻辑优化

  • 输入完成自动跳转:日输入完跳月份,月份完跳年份,年份逐位跳转
  • 删除回退逻辑:当前位为空时,自动跳转到前一位;年份第一位为空时,回退到月份第二位,以此类推

完整优化代码示例

struct Test: View {
    @StateObject var testViewModel: TestViewModel = .init()
    @FocusState var activeField: BirthdayFields?
    
    // 分组管理输入位索引
    private let dayIndices = [0,1]
    private let monthIndices = [2,3]
    private let yearIndices = [4,5,6,7]
    
    var body: some View {
        ZStack {
            Color.black.ignoresSafeArea()
            VStack {
                Spacer()
                BirthdayField()
                    .padding()
                Button{Task {await testViewModel.letsDate()}} label: {
                    Text("确认生日")
                        .keyboardType(.numberPad)
                        .padding()
                        .frame(maxWidth: .infinity)
                        .background{
                            RoundedRectangle(cornerRadius: 10, style: .continuous)
                                .fill(.yellow)
                        }
                }
                .disabled(checkStates())
                .opacity(checkStates() ? 0.4 : 1)
                Spacer()
            }
            .onChange(of: testViewModel.dobFields) { newValue in
                handleDOBInput(value: newValue)
            }
            .onAppear {
                activeField = .day1 // 默认聚焦第一个日输入框
            }
        }
    }
    
    @ViewBuilder
    func BirthdayField() -> some View {
        HStack(spacing: 6) {
            // 日输入框
            ForEach(dayIndices, id: \.self) { index in
                VStack {
                    TextField(index == 0 ? "D" : "D", text: $testViewModel.dobFields[index])
                        .foregroundColor(.gray)
                        .keyboardType(.numberPad)
                        .textContentType(.dateTime)
                        .multilineTextAlignment(.center)
                        .focused($activeField, equals: activeStateForIndex(index: index))
                    Rectangle()
                        .fill(activeField == activeStateForIndex(index: index) ? .blue : .gray.opacity(0.4))
                        .frame(height: 4)
                }
            }
            
            // 视觉分隔符
            Text("/")
                .foregroundColor(.gray)
                .font(.title)
            
            // 月输入框
            ForEach(monthIndices, id: \.self) { index in
                VStack {
                    TextField(index == 2 ? "M" : "M", text: $testViewModel.dobFields[index])
                        .foregroundColor(.gray)
                        .keyboardType(.numberPad)
                        .textContentType(.dateTime)
                        .multilineTextAlignment(.center)
                        .focused($activeField, equals: activeStateForIndex(index: index))
                    Rectangle()
                        .fill(activeField == activeStateForIndex(index: index) ? .blue : .gray.opacity(0.4))
                        .frame(height: 4)
                }
            }
            
            // 视觉分隔符
            Text("/")
                .foregroundColor(.gray)
                .font(.title)
            
            // 年输入框
            ForEach(yearIndices, id: \.self) { index in
                VStack {
                    TextField(index == 4 ? "Y" : "Y", text: $testViewModel.dobFields[index])
                        .foregroundColor(.gray)
                        .keyboardType(.numberPad)
                        .textContentType(.dateTime)
                        .multilineTextAlignment(.center)
                        .focused($activeField, equals: activeStateForIndex(index: index))
                    Rectangle()
                        .fill(activeField == activeStateForIndex(index: index) ? .blue : .gray.opacity(0.4))
                        .frame(height: 4)
                }
            }
        }
    }
    
    // 检查所有输入框是否填满
    func checkStates() -> Bool {
        testViewModel.dobFields.contains(where: { $0.isEmpty })
    }
    
    // 索引对应枚举
    func activeStateForIndex(index: Int) -> BirthdayFields {
        switch index {
        case 0: return .day1
        case 1: return .day2
        case 2: return .month1
        case 3: return .month2
        case 4: return .year1
        case 5: return .year2
        case 6: return .year3
        case 7: return .year4
        default: return .day1
        }
    }
    
    // 优化后的枚举
    enum BirthdayFields {
        case day1, day2, month1, month2, year1, year2, year3, year4
    }
    
    // 处理生日输入逻辑
    func handleDOBInput(value: [String]) {
        // 限制每个输入框只能有1个字符
        for index in 0..<8 {
            if value[index].count > 1 {
                testViewModel.dobFields[index] = String(value[index].last!)
            }
        }
        
        let currentValue = testViewModel.dobFields
        
        // 格式校验与自动跳转/回退
        switch activeField {
        case .day1:
            // 日第一位只能是0/1/2/3
            if let char = currentValue[0].first, !["0","1","2","3"].contains(char) {
                testViewModel.dobFields[0] = ""
                return
            }
            if !currentValue[0].isEmpty {
                activeField = .day2
            }
        case .day2:
            // 日第二位限制
            if currentValue[0] == "3" {
                if let char = currentValue[1].first, !["0","1"].contains(char) {
                    testViewModel.dobFields[1] = ""
                    return
                }
            }
            if !currentValue[1].isEmpty {
                activeField = .month1
            } else if currentValue[0].isEmpty == false {
                activeField = .day1
            }
        case .month1:
            // 月第一位只能是0/1
            if let char = currentValue[2].first, !["0","1"].contains(char) {
                testViewModel.dobFields[2] = ""
                return
            }
            if !currentValue[2].isEmpty {
                activeField = .month2
            } else {
                activeField = .day2
            }
        case .month2:
            // 月第二位限制
            if currentValue[2] == "1" {
                if let char = currentValue[3].first, !["0","1","2"].contains(char) {
                    testViewModel.dobFields[3] = ""
                    return
                }
            }
            if !currentValue[3].isEmpty {
                activeField = .year1
            } else if currentValue[2].isEmpty == false {
                activeField = .month1
            }
        case .year1:
            if !currentValue[4].isEmpty {
                activeField = .year2
            } else {
                activeField = .month2
            }
        case .year2:
            if !currentValue[5].isEmpty {
                activeField = .year3
            } else {
                activeField = .year1
            }
        case .year3:
            if !currentValue[6].isEmpty {
                activeField = .year4
            } else {
                activeField = .year2
            }
        case .year4:
            if currentValue[7].isEmpty {
                activeField = .year3
            }
        case .none:
            break
        }
        
        // 拼接符合DD MM YYYY格式的字符串
        let day = currentValue[0] + currentValue[1]
        let month = currentValue[2] + currentValue[3]
        let year = currentValue[4] + currentValue[5] + currentValue[6] + currentValue[7]
        testViewModel.dobText = "\(day) \(month) \(year)"
    }
}

// 对应ViewModel示例
class TestViewModel: ObservableObject {
    @Published var dobFields: [String] = Array(repeating: "", count: 8)
    @Published var dobText: String = ""
    
    func letsDate() async {
        print("生日:\(dobText)")
        // 后续业务逻辑
    }
}

关键优化点

  • 加入视觉分隔符,明确提示用户输入格式
  • 针对每个输入位做规则校验,避免无效输入
  • 优化自动跳转与删除回退逻辑,符合用户输入习惯
  • 自动拼接生成符合要求的DD MM YYYY格式字符串

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 17:55:30