书籍类项目中如何让UITextView最后一行两端对齐?
我之前做类书籍阅读项目时也碰到过这个棘手的问题——UITextView默认不对段落内的非结尾行做强制两端对齐,跨页场景下这种表现确实会破坏书籍的阅读质感,还好有几个能保留原生文本选择/复制功能的解决方案:
方案一:自定义UITextView布局逻辑(最推荐)
通过修改TextKit的布局回调,精准判断当前行是否为跨页段落的中间行,强制让它两端对齐:
- 自定义一个继承自
UITextView的类,重写布局管理器的回调方法:
class JustifiedTextView: UITextView { override func layoutManager(_ layoutManager: NSLayoutManager, shouldSetLineFragmentRect lineFragmentRect: UnsafeMutablePointer<CGRect>, lineFragmentUsedRect: UnsafeMutablePointer<CGRect>, baselineOffset: UnsafeMutablePointer<CGFloat>, in textContainer: NSTextContainer, forGlyphRange glyphRange: NSRange) -> Bool { // 转换字形范围为字符范围,再获取当前行所属的段落范围 let characterRange = layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil) let paragraphRange = textStorage.string.paragraphRange(for: characterRange) // 仅当当前行是段落的中间行(不是段落结尾,也不是整个文本的最后一行)时,强制撑满宽度 if characterRange.upperBound == paragraphRange.upperBound - 1 && characterRange.upperBound < textStorage.length - 1 { var rect = lineFragmentRect.pointee rect.size.width = textContainer.size.width lineFragmentRect.pointee = rect } return true } }
- 把项目中原有的
UITextView替换成这个自定义类即可,它会自动识别跨页场景,不影响原生的文本交互功能。
方案二:手动调整最后一行字符间距
这个方法更直接,通过计算最后一行的宽度差,动态调整字符间距来撑满页面:
在每个UITextView加载完文本后,调用以下方法:
func justifyLastLineForPaginatedTextView(_ textView: UITextView) { guard let text = textView.text, !text.isEmpty else { return } let layoutManager = textView.layoutManager let textContainer = textView.textContainer // 获取最后一行的字形范围和对应的字符范围 let lastCharRange = NSRange(location: text.count - 1, length: 1) let lastGlyphRange = layoutManager.glyphRange(forCharacterRange: lastCharRange, actualCharacterRange: nil) var lastLineGlyphRange = NSRange() layoutManager.lineFragmentRect(forGlyphAt: lastGlyphRange.location, effectiveRange: &lastLineGlyphRange) let lastLineCharRange = layoutManager.characterRange(forGlyphRange: lastLineGlyphRange, actualGlyphRange: nil) // 判断当前行是否为段落中间行 let paragraphRange = text.paragraphRange(for: lastCharRange) if lastLineCharRange.upperBound != paragraphRange.upperBound { let lastLineText = (text as NSString).substring(with: lastLineCharRange) let attributes = textView.typingAttributes let lineWidth = (lastLineText as NSString).size(withAttributes: attributes).width let availableWidth = textContainer.size.width - textContainer.lineFragmentPadding * 2 // 计算需要的字符间距,避免字符间距过大导致排版怪异 if lineWidth < availableWidth && lastLineText.count > 1 { let spacing = (availableWidth - lineWidth) / CGFloat(lastLineText.count - 1) let attributedText = NSMutableAttributedString(attributedString: textView.attributedText!) attributedText.addAttribute(.kern, value: spacing, range: lastLineCharRange) textView.attributedText = attributedText } } }
注意:如果文本有动态调整(比如字体切换、字号修改),需要重新调用这个方法。
方案三:iOS 16+ 用TextKit 2简化实现
如果你的项目只需要支持iOS 16及以上版本,TextKit 2提供了更智能的排版控制,直接配置段落样式即可:
let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = .justified // TextKit 2会自动识别跨页的段落中间行,强制两端对齐 paragraphStyle.lineBreakMode = .byWordWrapping paragraphStyle.hyphenationFactor = 0.8 // 可选,开启连字符优化长单词排版 let attributedText = NSAttributedString(string: yourPageText, attributes: [ .paragraphStyle: paragraphStyle, .font: UIFont.systemFont(ofSize: 16) ]) textView.attributedText = attributedText // 开启TextKit 2 textView.useTextKit2 = true
不管用哪个方案,都建议多测试不同字体、字号和屏幕尺寸下的效果,避免出现字符间距过大或者排版错乱的问题。所有方案都基于系统原生能力实现,完全保留UITextView的文本选择、复制等交互功能。
内容的提问来源于stack exchange,提问作者svojtko
相关产品推荐
相关产品推荐

