如何根据UILabel高度分割NSMutableAttributedString并适配多标签展示
这需求我之前做资讯类APP的时候碰到过,核心难点是不能粗暴按字符数分割富文本,得用TextKit的布局能力精准计算每个Label能容纳的文本范围,这样分割后的内容既不会断字,也能完整保留原始的样式(比如字体、颜色、链接这些)。下面给你一套可直接复用的方案:
核心思路
利用TextKit的NSLayoutManager、NSTextContainer和NSTextStorage三个核心类,模拟UILabel的文本布局逻辑,计算出指定宽度/高度下能容纳的文本范围,循环分割直到所有内容都被分配到对应的AttributedString片段中,最后逐个创建Label展示。
实现步骤
1. 编写富文本分割工具方法
这个方法会帮你把原始的NSAttributedString分割成符合尺寸要求的片段数组:
func splitAttributedString(_ attributedText: NSAttributedString, maxWidth: CGFloat, maxHeight: CGFloat) -> [NSAttributedString] { var splitResults = [NSAttributedString]() let textStorage = NSTextStorage(attributedString: attributedText) let layoutManager = NSLayoutManager() textStorage.addLayoutManager(layoutManager) var remainingTextRange = NSRange(location: 0, length: attributedText.length) while remainingTextRange.length > 0 { // 创建一个和目标Label尺寸一致的文本容器 let textContainer = NSTextContainer(size: CGSize(width: maxWidth, height: maxHeight)) textContainer.lineBreakMode = .byWordWrapping // 保持和Label一致的换行规则 layoutManager.addTextContainer(textContainer) // 计算当前容器能容纳的字形范围,再转成字符范围 let glyphRange = layoutManager.glyphRange(for: textContainer) let characterRange = layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil) // 取出当前片段并加入结果数组 let subString = attributedText.attributedSubstring(from: characterRange) splitResults.append(subString) // 更新剩余文本的范围 remainingTextRange = NSRange( location: characterRange.location + characterRange.length, length: attributedText.length - (characterRange.location + characterRange.length) ) // 移除当前容器,准备下一次计算 layoutManager.removeTextContainer(textContainer) } return splitResults }
2. 在视图中使用分割结果并创建多个Label
假设你有一个父视图contentView用来承载这些Label,指定的单Label最大高度是100,宽度是父视图的可用宽度:
// 你的动态生成富文本 let dynamicAttributedText: NSMutableAttributedString = ... let maxLabelWidth = contentView.bounds.width - 20 // 左右各留10pt间距 let maxSingleLabelHeight: CGFloat = 100 // 你设定的高度阈值 // 先判断是否需要分割:用临时Label计算原始文本的总高度 let tempLabel = UILabel() tempLabel.attributedText = dynamicAttributedText tempLabel.numberOfLines = 0 tempLabel.widthAnchor.constraint(equalToConstant: maxLabelWidth).isActive = true tempLabel.sizeToFit() if tempLabel.frame.height <= maxSingleLabelHeight { // 不需要分割,直接添加一个Label let singleLabel = UILabel() singleLabel.attributedText = dynamicAttributedText singleLabel.numberOfLines = 0 singleLabel.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(singleLabel) NSLayoutConstraint.activate([ singleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), singleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), singleLabel.topAnchor.constraint(equalTo: contentView.topAnchor) ]) } else { // 需要分割,调用工具方法获取片段数组 let splitStrings = splitAttributedString(dynamicAttributedText, maxWidth: maxLabelWidth, maxHeight: maxSingleLabelHeight) var previousLabel: UILabel? for (index, string) in splitStrings.enumerated() { let label = UILabel() label.attributedText = string label.numberOfLines = 0 label.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(label) // 设置布局约束 NSLayoutConstraint.activate([ label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), label.heightAnchor.constraint(lessThanOrEqualToConstant: maxSingleLabelHeight) ]) // 处理Label之间的间距 if let prevLabel = previousLabel { label.topAnchor.constraint(equalTo: prevLabel.bottomAnchor, constant: 8).isActive = true } else { label.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true } // 如果是最后一个Label,绑定父视图底部约束(可选,根据你的布局需求调整) if index == splitStrings.count - 1 { contentView.bottomAnchor.constraint(equalTo: label.bottomAnchor, constant: 10).isActive = true } previousLabel = label } }
关键注意事项
- 保持换行模式一致:分割时
textContainer的lineBreakMode要和实际UILabel的一致,不然会出现分割位置和显示位置不匹配的问题。 - 样式完整保留:这个方法基于TextKit的布局计算,不会破坏原始富文本的任何样式(比如字体、颜色、段落样式、链接等)。
- 布局环境匹配:临时Label的宽度、字体等属性要和实际展示的Label完全一致,否则高度计算会不准确。
- 滚动视图适配:如果父视图是
UIScrollView,记得通过AutoLayout让滚动视图的contentSize自动适应所有Label的高度,或者手动计算并设置。
内容的提问来源于stack exchange,提问作者Jojo Narte
相关产品推荐
相关产品推荐

