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
相关产品推荐
相关产品推荐

