使用TextKit 1替换UITextView的NSLayoutManager时崩溃求助
问题描述
在Storyboard中创建了采用TextKit 1的UITextView,尝试替换其内部的NSLayoutManager,但调用该文本视图的.sizeThatFits()(或其他布局相关方法)时,触发未捕获异常:
*** -[NSLayoutManager glyphRangeForTextContainer:]: given container does not appear in the list of containers for this NSLayoutManager.
替换布局管理器的代码如下:
let layoutManager = MyLayoutManager() layoutManager.textStorage = self.textStorage layoutManager.addTextContainer(self.textContainer) self.textContainer.replaceLayoutManager(layoutManager) // etc.
尝试在initWithCoder和awakeFromNib中执行上述操作,且确认布局管理器的textContainers数组确实包含当前textContainer。即使使用原生NSLayoutManager而非自定义子类,崩溃依然发生。
通过堆栈跟踪发现崩溃触发自TextKit 1的私有方法,且进入.sizeThatFits()前,layoutManager.textContainers.contains(self.textContainer)返回true。自定义布局管理器的glyphRangeForTextContainer:方法并未被私有方法调用,怀疑私有方法仍在引用内存中旧布局管理器的残留。该问题在TextKit 1的空项目中也可复现,暂无法确定是系统Bug还是自身操作错误。
解决方案
问题出在TextKit组件的关联顺序和旧布局管理器的解除逻辑上,UITextView初始化时会自动创建一套默认的NSTextStorage、NSLayoutManager、NSTextContainer关联关系,直接替换时可能未彻底切断旧关联,导致私有方法仍引用旧布局管理器。
正确的替换步骤应该是:
- 先解除旧布局管理器与
textStorage、textContainer的关联 - 再将新布局管理器与
textStorage、textContainer建立关联 - 最后更新UITextView的
layoutManager属性
修改后的代码示例:
override func awakeFromNib() { super.awakeFromNib() // 获取旧布局管理器并解除关联 let oldLayoutManager = self.layoutManager oldLayoutManager?.removeTextContainer(self.textContainer) self.textStorage?.removeLayoutManager(oldLayoutManager) // 创建并配置新布局管理器 let newLayoutManager = MyLayoutManager() self.textStorage?.addLayoutManager(newLayoutManager) newLayoutManager.addTextContainer(self.textContainer) // 更新UITextView的布局管理器引用 self.layoutManager = newLayoutManager }
关键说明
- 必须先移除旧布局管理器的关联:TextKit的三个组件是双向绑定的,仅替换一侧会导致残留引用,私有方法可能仍通过旧关联访问错误的布局管理器
- 优先使用
textStorage.addLayoutManager()而非直接设置layoutManager.textStorage:前者会正确处理双向关联,后者可能遗漏部分绑定逻辑 - 在
awakeFromNib中执行替换:此时UITextView的所有组件已完成初始化,避免在initWithCoder中操作时组件未完全创建导致的异常
内容的提问来源于stack exchange,提问作者Antifonic
相关产品推荐
相关产品推荐

