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

