如何为NSTimeInterval类型值提供经校验的Cocoa输入实现
macOS 10.13+ 兼容Objective-C/Swift的分秒格式时间输入控件实现
核心疑问解答
- 是否可以直接将字符串传入NSTimeInterval处理?
完全不行。NSTimeInterval本质是double类型的别名,属于基础数值类型,没有内置的字符串解析能力,不存在“直接传入字符串处理”的用法。如果尝试通过强制类型转换、doubleValue等方式直接从任意格式字符串取值,会得到不可预期的脏值,完全无法满足输入校验要求。 - 如何将NSTimeInterval反向转换为「分钟:秒.小数位」格式字符串?
不能靠系统内置的日期格式化器(NSDateFormatter格式化的是时钟时间,会受时区、进位规则影响,不支持任意时长的分秒显示),需要手动拆分时间分量做格式化,具体逻辑见下文实现。
控件选型
直接继承NSTextField实现,无SwiftUI依赖,从macOS 10.12开始API就完全稳定,兼容10.13.x版本没有问题,同时支持Objective-C和Swift混编。
输入校验&正向解析规则(核心,防止无效值覆盖配置)
所有校验逻辑在输入框结束编辑(失焦、用户按回车确认)时触发,输入过程中不打断用户操作:
- 先用正则
^([0-9]+):([0-5]?[0-9])(?:\.([0-9]{1,3}))?$做格式初筛,不匹配规则直接判定无效 - 拆分匹配到的三个分量:
- 分钟分量:非负整数,不设上限,支持长时长输入
- 秒分量:必须在0-59区间,超出范围判定无效
- 小数分量:1-3位数字,不足3位时右侧补0转成毫秒值(比如
.5补为500即500毫秒,.12补为120即120毫秒)
- 计算总时间值:
- 总毫秒值 = 分钟601000 + 秒*1000 + 小数位毫秒值
- NSTimeInterval值 = 总毫秒值 / 1000.0
- 最终有效性判定:总毫秒值必须大于0,零值、负值都判定为无效
校验不通过时,直接将输入框内容回滚为上一次存储的有效值,不要清空输入框,也不要执行任何NSUserDefaults写入操作,从流程上杜绝无效值覆盖原有配置。
反向转换(时间值转显示字符串)逻辑
- 统一把输入的时间值转成整数毫秒值避免浮点误差:如果传入的是NSTimeInterval,先执行
(timeInterval * 1000).rounded()得到整数毫秒 - 拆分时间分量:
- 总分钟 = 总毫秒 / 60000
- 剩余毫秒 = 总毫秒 % 60000
- 秒值 = 剩余毫秒 / 1000
- 小数毫秒 = 剩余毫秒 % 1000
- 格式化拼接:
- 秒值固定补0为2位长度(比如3秒显示为
03) - 小数毫秒部分去掉末尾无意义的0,如果小数部分为0则直接省略小数点和后续内容
- 秒值固定补0为2位长度(比如3秒显示为
示例转换结果:
- 123456毫秒 → 2分3秒456毫秒 → 显示
2:03.456 - 61000毫秒 → 1分1秒0毫秒 → 显示
1:01 - 90100毫秒 → 1分30秒100毫秒 → 显示
1:30.1
核心代码示例
Swift 实现
/// 解析输入字符串为毫秒值,返回nil代表无效输入 func parseTimeInput(_ input: String) -> Int64? { let pattern = #"^([0-9]+):([0-5]?[0-9])(?:\.([0-9]{1,3}))?$"# guard let regex = try? NSRegularExpression(pattern: pattern), let match = regex.firstMatch(in: input, range: NSRange(input.startIndex..., in: input)), let minRange = Range(match.range(at: 1), in: input), let secRange = Range(match.range(at: 2), in: input), let minutes = Int64(input[minRange]), let seconds = Int64(input[secRange]) else { return nil } var ms: Int64 = 0 if let fracRange = Range(match.range(at: 3), in: input) { var fracStr = String(input[fracRange]) while fracStr.count < 3 { fracStr.append("0") } guard let fracVal = Int64(fracStr) else { return nil } ms = fracVal } let totalMs = minutes * 60000 + seconds * 1000 + ms return totalMs > 0 ? totalMs : nil } /// 毫秒值转显示字符串 func formatTimeDisplay(totalMs: Int64) -> String { let minutes = totalMs / 60000 let remain = totalMs % 60000 let seconds = remain / 1000 let frac = remain % 1000 if frac == 0 { return String(format: "%lld:%02lld", minutes, seconds) } var fracStr = String(format: "%03lld", frac) while fracStr.last == "0" { fracStr.removeLast() } return String(format: "%lld:%02lld.%@", minutes, seconds, fracStr) } // 结束编辑时的校验写入逻辑 func controlTextDidEndEditing(_ obj: Notification) { guard let field = obj.object as? NSTextField else { return } let input = field.stringValue if let validMs = parseTimeInput(input) { UserDefaults.standard.set(validMs, forKey: "customTimeLength") } else { // 回滚到上一次有效值 let lastMs = UserDefaults.standard.longLong(forKey: "customTimeLength") field.stringValue = formatTimeDisplay(totalMs: lastMs) } }
Objective-C 核心调用逻辑
- (void)controlTextDidEndEditing:(NSNotification *)obj { NSTextField *field = obj.object; NSString *input = field.stringValue; NSNumber *validMs = [self parseInputToMs:input]; if (validMs) { [[NSUserDefaults standardUserDefaults] setObject:validMs forKey:@"customTimeLength"]; } else { int64_t lastMs = [[NSUserDefaults standardUserDefaults] longLongForKey:@"customTimeLength"]; field.stringValue = [self formatMsToDisplay:lastMs]; } }
内容的提问来源于stack exchange,提问作者kode54
相关产品推荐
相关产品推荐

