SwiftUI中单位数TextField输入与自动焦点跳转问题
解决SwiftUI中时间输入框自动焦点跳转问题
问题描述
我需要创建一个hh:mm格式的时间输入表单,要求严格校验输入有效性(比如24小时制下小时不能超过23),且不想用Picker(选60分钟太繁琐)。于是我拆了四个TextField分别对应时十位、时个位、分十位、分个位,添加了数字校验逻辑,希望输入有效数字后自动跳转到下一个输入框。
我用了一个遵循Hashable的枚举标记每个输入框,校验功能正常,但输入有效数字后,焦点虽然离开当前框,下一个框却没获得键盘焦点;同时在MacBook Pro模拟器运行时,调试器报错:-[RTIInputSystemClient remoteTextInputSessionWithID:performInputOperation:] perform input operation requires a valid sessionID
我是Swift新手,官方文档太简洁看不懂,想知道这类功能的正确实现方式,希望得到示例代码和建议。
原始代码:
// // ContentView.swift // StackOverflowExample // // Created by Mike Olson on 10/8/23. // import SwiftUI import Combine struct ContentView: View { // buch of hard-coded infra to automatically advance focus through the form enum Field: Hashable { case f_ch1 case f_ch2 case f_cm1 case f_cm2 } @FocusState private var focusedField: Field? // Clock digits @State private var ch1 = "" @State private var ch2 = "" @State private var cm1 = "" @State private var cm2 = "" var body: some View { VStack { HStack { TextField("h", text: $ch1) .focusable(true, interactions: .edit) .focused($focusedField, equals: .f_ch1) .font(.title3) .keyboardType(.numberPad) .onReceive(Just(ch1)) { newValue in let filtered = newValue.filter { "012".contains($0) } // first digit can be 0, 1, 2 if filtered != newValue { self.ch1 = filtered } else { focusedField = .f_ch2 } } TextField("h", text: $ch2) .focusable(true, interactions: .edit) .focused($focusedField, equals: .f_ch2) .font(.title3) .keyboardType(.numberPad) .onReceive(Just(ch2)) { newValue in let filtered = newValue.filter { (self.ch1 == "2" ? "0123" : "0123456789").contains($0) } // if the first digit's a 2, the second can't be bigger than 3 if filtered != newValue { self.ch2 = filtered } else { focusedField = .f_cm1 } } Text(":") .font(.title3) TextField("m", text: $cm1) .focusable(true, interactions: .edit) .focused($focusedField, equals: .f_cm1) .font(.title3) .keyboardType(.numberPad) .onReceive(Just(cm1)) { newValue in let filtered = newValue.filter { "012345".contains($0) } // first minute digit is 0-5 if filtered != newValue { self.cm1 = filtered } } TextField("m", text: $cm2) .focusable(true, interactions: .edit) .focused($focusedField, equals: .f_cm2) .font(.title3) .keyboardType(.numberPad) .onReceive(Just(cm2)) { newValue in let filtered = newValue.filter { "0123456789".contains($0) } if filtered != newValue { self.cm2 = filtered } } } .frame(maxWidth: 100) } .frame(width: 200, height: 200) } } #Preview { ContentView() }
问题原因与修复方案
核心问题
之前的代码在文本变化时就触发焦点跳转,包括初始空值、手动修正输入的场景,导致焦点切换过于频繁,系统来不及正确响应焦点请求,同时也会触发模拟器的输入会话错误。
改进后的代码
import SwiftUI import Combine struct ContentView: View { enum Field: Hashable { case hourTens, hourUnits, minuteTens, minuteUnits } @FocusState private var focusedField: Field? @State private var hourTens = "" @State private var hourUnits = "" @State private var minuteTens = "" @State private var minuteUnits = "" // 辅助计算完整时间,用于后续验证或提交 private var fullTime: String { "\(hourTens)\(hourUnits):\(minuteTens)\(minuteUnits)" } var body: some View { VStack(spacing: 20) { HStack(spacing: 8) { // 时十位 TextField("H", text: $hourTens) .focused($focusedField, equals: .hourTens) .font(.title3) .keyboardType(.numberPad) .frame(width: 30, alignment: .center) .onReceive(Just(hourTens)) { newValue in // 只保留0-2的数字,且限制长度为1 let filtered = newValue.filter { "012".contains($0) }.prefix(1) let newText = String(filtered) if newText != newValue { hourTens = newText } else if newText.count == 1 { // 输入有效且长度为1时,跳转到下一个输入框 focusedField = .hourUnits } } // 时个位 TextField("H", text: $hourUnits) .focused($focusedField, equals: .hourUnits) .font(.title3) .keyboardType(.numberPad) .frame(width: 30, alignment: .center) .onReceive(Just(hourUnits)) { newValue in // 根据时十位决定允许的数字:如果是2,只能0-3,否则0-9 let allowedChars = hourTens == "2" ? "0123" : "0123456789" let filtered = newValue.filter { allowedChars.contains($0) }.prefix(1) let newText = String(filtered) if newText != newValue { hourUnits = newText } else if newText.count == 1 { focusedField = .minuteTens } } Text(":") .font(.title3) // 分十位 TextField("M", text: $minuteTens) .focused($focusedField, equals: .minuteTens) .font(.title3) .keyboardType(.numberPad) .frame(width: 30, alignment: .center) .onReceive(Just(minuteTens)) { newValue in let filtered = newValue.filter { "012345".contains($0) }.prefix(1) let newText = String(filtered) if newText != newValue { minuteTens = newText } else if newText.count == 1 { focusedField = .minuteUnits } } // 分个位 TextField("M", text: $minuteUnits) .focused($focusedField, equals: .minuteUnits) .font(.title3) .keyboardType(.numberPad) .frame(width: 30, alignment: .center) .onReceive(Just(minuteUnits)) { newValue in let filtered = newValue.filter { "0123456789".contains($0) }.prefix(1) let newText = String(filtered) if newText != newValue { minuteUnits = newText } } } // 测试用:显示完整时间 Text("输入的时间:\(fullTime)") .font(.caption) } .frame(width: 200, height: 200) .onAppear { // 初始焦点设置为时十位 focusedField = .hourTens } } } #Preview { ContentView() }
关键改进点
- 限制输入长度:用
.prefix(1)确保每个TextField最多只能输入1个字符,避免多次触发跳转逻辑 - 调整跳转时机:只有当输入有效且文本长度为1时,才设置下一个焦点,避免空值或无效输入时的错误跳转
- 优化语义命名:把枚举和状态变量改成更易读的名称,提升代码可维护性
- 初始焦点设置:在
.onAppear中设置初始焦点,用户打开页面即可直接输入 - 移除冗余设置:去掉
.focusable(true, interactions: .edit),TextField默认支持编辑聚焦
效果说明
修复后,输入有效数字会自动跳转到下一个输入框,模拟器的输入会话错误也会消失。同时代码结构更清晰,校验逻辑更严谨,符合24小时制的时间规则。
内容的提问来源于stack exchange,提问作者mikeolson
相关产品推荐
相关产品推荐

