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

Swift自定义指定范围富文本样式 NSRange越界崩溃问题排查

问题描述

我是Swift开发初学者,希望实现自定义Label的字体效果:部分字符使用较大字号,其余字符使用较小字号,效果可参考示例图:
点击打开示例图

具体需求为:货币符号、小数点及后续小数部分的字号,小于整数部分数字的字号。

原有实现代码如下:

// 获取字符串中小数点的位置索引
func getDecimalIndex(text: String) -> Int {
    let myText = text.firstIndex(of: ".")
    return myText?.utf16Offset(in: text) ?? 0
}

func setupLabel(text: String) {
    guard !text.isEmpty else { return }
    
    let attributedString = NSMutableAttributedString(string: text)
    let decimalStart = getDecimalIndex(text: attributedString.string)
    attributedString.addAttribute(.font,
                                  value: UIFont.systemFont(ofSize: 22),
                                  range: NSRange(location: decimalStart, length: attributedString.length))
    
    myLabel.attributedText = attributedString
}

setupLabel(text: "$1100.33")
setupLabel(text: "1100.33€")

运行时在添加字体属性的代码行触发崩溃,崩溃代码:

attributedString.addAttribute(.font,
                                  value: UIFont.systemFont(ofSize: 22),
                                  range: NSRange(location: decimalStart, length: attributedString.length))

控制台错误信息:

Terminating app due to uncaught exception 'NSRangeException', reason: 'NSMutableRLEArray objectAtIndex:effectiveRange:: Out of bounds'


出错原因

崩溃的核心原因是NSRange参数设置错误导致越界:
NSRange的length参数代表从location位置开始向后延伸的字符长度,不是字符串的总长度。以输入"$1100.33"为例,字符串总长度为7,小数点位置在索引4处,此时传入length: 7,实际访问的范围是索引4到索引4+7=11,远超字符串最大索引6,直接触发越界异常。

原有逻辑还存在两处漏洞:

  • 字符串不含小数点时,getDecimalIndex返回0,会给整段文本设置同一字号,不符合整数部分放大的需求
  • 未覆盖货币符号的小字号设置,仅处理了小数点后的内容
修复代码

调整实现逻辑:先给整段文本全局设置小字号,再单独定位整数数字区间设置大字号,从根源避免范围计算错误,同时兼容货币符号在开头/结尾、无小数点的场景:

func setupLabel(text: String, bigFont: UIFont = .systemFont(ofSize: 22), smallFont: UIFont = .systemFont(ofSize: 14)) {
    guard !text.isEmpty else { return }
    let attrStr = NSMutableAttributedString(string: text)
    let fullRange = NSRange(location: 0, length: attrStr.length)
    // 全局默认小字号
    attrStr.addAttribute(.font, value: smallFont, range: fullRange)
    
    if let dotIndex = text.firstIndex(of: ".") {
        // 有小数点时,整数部分为0到小数点前的区间
        let dotOffset = dotIndex.utf16Offset(in: text)
        let integerRange = NSRange(location: 0, length: dotOffset)
        attrStr.addAttribute(.font, value: bigFont, range: integerRange)
    } else {
        // 无小数点时,提取中间连续数字的区间设置大字号
        let digitSet = CharacterSet.decimalDigits
        var intStart = 0
        var intEnd = text.count
        // 定位前置货币符号的结束位置
        for (idx, c) in text.enumerated() {
            if c.unicodeScalars.allSatisfy({ digitSet.contains($0) }) {
                intStart = idx
                break
            }
        }
        // 定位后置货币符号的起始位置
        for (idx, c) in text.reversed().enumerated() {
            if c.unicodeScalars.allSatisfy({ digitSet.contains($0) }) {
                intEnd = text.count - idx
                break
            }
        }
        let integerRange = NSRange(location: intStart, length: intEnd - intStart)
        attrStr.addAttribute(.font, value: bigFont, range: integerRange)
    }
    
    myLabel.attributedText = attrStr
}

// 测试用例
setupLabel(text: "$1100.33")
setupLabel(text: "1100.33€")
setupLabel(text: "¥888")
setupLabel(text: "999£")

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 19:36:19