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

iOS16下UITextView访问layoutManager属性崩溃问题求助

iOS16下UITextView访问layoutManager崩溃的修复方案与TextKit2适配

问题背景

原有代码稳定运行多年,但在iOS16系统中,访问UITextView的layoutManager属性(layoutManager = textView.layoutManager)时出现崩溃。系统会自动触发TextKit 1兼容模式,但当文本首词为非拉丁语时,部分UITextView仍会抛出NSRangeException,提示字符范围越界。

崩溃日志

2022-09-15 13:29:55.532693-0700 DictionaryApp[1101:95240] [Text] UITextView 0x1051cd600 正在切换到TextKit 1兼容模式,原因是访问了其layoutManager。可在`void _UITextViewEnablingCompatibilityMode(UITextView *__strong, BOOL)`断点调试。
2022-09-15 13:29:55.543047-0700 DictionaryApp[1101:95240] !!! _NSGlyphTreeInvalidateGlyphsForCharacterRange 无效字符范围 1
2022-09-15 13:29:55.543166-0700 DictionaryApp[1101:95240] !!! _NSGlyphTreeInvalidateGlyphsForCharacterRange 字符计数不匹配
2022-09-15 13:32:06.118905-0700 DictionaryApp[1101:95240] *** 因未捕获的异常'NSRangeException'终止应用,原因: '*** -[NSBigMutableString _getBlockStart:end:contentsEnd:forRange:stopAtLineSeparators:]: 范围 {5, 99} 越界;字符串长度 103'
*** 首次抛出调用栈:
(0x1ab8f2248 0x1a4cbfa68 0x1a5d7c484 0x1b5b42260 0x1b5b5710c 0x1b5b55a14 0x1b5b73a20 0x1b5b73924 0x1b5b44df4 0x1b5b43cf4 0x1b5b639c0 0x1b5b61b08 0x1b5b61778 0x1b5b6125c 0x1ae9adcc4 0x1adaa76d0 0x1ae9ada3c 0x1adb8bfa0 0x1025ce384 0x102474d9c 0x102475154 0x1adb35164 0x1adea2dfc 0x1adc1cb94 0x1adafd1dc 0x1adba9358 0x1ae42a1d8 0x1adb6dd18 0x1adb72674 0x1adb71944 0x1adb71000 0x1adbb8d64 0x1adec14dc 0x1ab9be22c 0x1ab9ca614 0x1ab94e51c 0x1ab963eb8 0x1ab9691e4 0x1e4789368 0x1ade18d88 0x1ade189ec 0x102661fb0 0x1c9c8d948)
libc++abi: 因未捕获的NSException类型异常终止
*** 因未捕获的异常'NSRangeException'终止应用,原因: '*** -[NSBigMutableString _getBlockStart:end:contentsEnd:forRange:stopAtLineSeparators:]: 范围 {5, 99} 越界;字符串长度 103'
因未捕获的NSException类型异常终止

崩溃调用栈

崩溃调用栈

原代码

static func didTapTerm(_ sender: UITapGestureRecognizer) -> String? {
  guard let senderView = sender.view, (senderView is UITextView) else {
    return nil
  }

  let textView = senderView as! UITextView,
  layoutManager = textView.layoutManager
   
  var location = sender.location(in: textView)
  location.x -= textView.textContainerInset.left
  location.y -= textView.textContainerInset.top
   
  // 获取点击位置的字符索引
  let textContainer = textView.textContainer,
  characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil),
  textStorage = textView.textStorage
   
  guard characterIndex < textStorage.length else {
    return nil
  }
   
  var range = NSRange()
  if let phrase = textStorage.attribute(customAttributedStringKey,
                   at: characterIndex,
                   effectiveRange: &range) {
    if let ph = phrase as? String {
      // 更新UI ...
    }
    return phrase as? String
  }
  return nil
}

修复方案

方案一:临时兼容TextKit 1(避免触发兼容模式崩溃)

核心思路是绕开直接访问layoutManager,改用UITextView原生API获取点击位置的字符范围,同时加强边界校验:

static func didTapTerm(_ sender: UITapGestureRecognizer) -> String? {
  guard let textView = sender.view as? UITextView else {
    return nil
  }

  let location = sender.location(in: textView)
  // 直接用textView的API获取字符范围,无需手动计算inset
  guard let textRange = textView.characterRange(at: location),
        let startPosition = textRange.start,
        let characterIndex = textView.position(from: textView.beginningOfDocument, offset: startPosition.offset) else {
    return nil
  }

  let nsRange = textView.offset(from: textView.beginningOfDocument, to: characterIndex)
  let textStorage = textView.textStorage
  
  // 严格校验索引边界
  guard nsRange >= 0, nsRange < textStorage.length else {
    return nil
  }
  
  var effectiveRange = NSRange()
  if let phrase = textStorage.attribute(customAttributedStringKey, at: nsRange, effectiveRange: &effectiveRange) as? String {
    // 更新UI ...
    return phrase
  }
  return nil
}

方案二:全面适配TextKit 2(iOS16+)

iOS16及以后UITextView默认使用TextKit 2,无需触发兼容模式,直接使用TextKit 2的API处理:

@available(iOS 16.0, *)
static func didTapTermTextKit2(_ sender: UITapGestureRecognizer) -> String? {
  guard let textView = sender.view as? UITextView else {
    return nil
  }

  let location = sender.location(in: textView)
  // TextKit 2下直接用textRange(at:)获取点击范围
  guard let textRange = textView.textRange(at: location),
        let attributedText = textView.attributedText else {
    return nil
  }

  let startOffset = textView.offset(from: textView.beginningOfDocument, to: textRange.start)
  let textStorage = textView.textStorage
  
  guard startOffset < textStorage.length else {
    return nil
  }
  
  var effectiveRange = NSRange()
  if let phrase = textStorage.attribute(customAttributedStringKey, at: startOffset, effectiveRange: &effectiveRange) as? String {
    // 更新UI ...
    return phrase
  }
  return nil
}

额外注意事项

  • 若需支持iOS16以下版本,建议做版本判断,iOS16及以上用TextKit2方案,低版本用原兼容逻辑
  • 确保自定义属性customAttributedStringKey的设置逻辑没有字符范围相关的错误,避免触发后续的RangeException

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 11:05:30