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

书籍类项目中如何让UITextView最后一行两端对齐?

我之前做类书籍阅读项目时也碰到过这个棘手的问题——UITextView默认不对段落内的非结尾行做强制两端对齐,跨页场景下这种表现确实会破坏书籍的阅读质感,还好有几个能保留原生文本选择/复制功能的解决方案:

方案一:自定义UITextView布局逻辑(最推荐)

通过修改TextKit的布局回调,精准判断当前行是否为跨页段落的中间行,强制让它两端对齐:

  1. 自定义一个继承自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
    }
}
  1. 把项目中原有的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.06 17:08:11