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

如何为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写入操作,从流程上杜绝无效值覆盖原有配置。

反向转换(时间值转显示字符串)逻辑

  1. 统一把输入的时间值转成整数毫秒值避免浮点误差:如果传入的是NSTimeInterval,先执行(timeInterval * 1000).rounded()得到整数毫秒
  2. 拆分时间分量:
    • 总分钟 = 总毫秒 / 60000
    • 剩余毫秒 = 总毫秒 % 60000
    • 秒值 = 剩余毫秒 / 1000
    • 小数毫秒 = 剩余毫秒 % 1000
  3. 格式化拼接:
    • 秒值固定补0为2位长度(比如3秒显示为03)
    • 小数毫秒部分去掉末尾无意义的0,如果小数部分为0则直接省略小数点和后续内容

示例转换结果:

  • 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 03:12:23