TextKit
快速分页UIPageViewController
iOS, iPadOS
也许还支持 Mac Calalyst
?
Swift
|- UIViewController // 根视图, 可添加菜单显示, 手势操作等 |- UIPageController // 章节视图, 一页对应一章 | - UIPageController // 章节内容分页视图, 将单章内容进行分页显示 | | - UIViewController // 单页显示视图, 对应单页数据 | | |- UITextView // 文字视图 | | | | - UIViewController | | |- UITextView | | | | ... | | - UIPageController | | - UIViewController | | |- UITextView | | | | - UIViewController | | |- UITextView | | | | ... | | ... 复制代码
章节内容分页视图中, 只要在返回单页显示视图的代理中返回
nil
, 即可实现章节内容翻到最后一页时, 继续翻页翻到下一章节的逻辑
首先, 一定要先确定
好 TextView 的大小与内容间距, 即文字显示区域的大小
, 这将严重影响到分页后的数据能不能正常显示
其次, 首行缩进最好用空格代替, 而不是用 NSParagraphStyle
的 firstLineHeadIndent
属性来实现, 否则会出现某段落从中间被分开, 下一页依然被缩进的情况
首行缩进的空格数量可用以下逻辑计算:
let normalWidth = "你好".size(font: textFont).width // 请根据内容语言改变文字 let speaceWidth = " ".size(font: textFont).width // 一个空格的宽 let speaceCount = Int(normalWidth / speaceWidth) let speace = String(repeating: " ", count: speaceCount) 复制代码
然后在每段前添加空格
let result = content.string.components(separatedBy: "\n").map { "\(speace)\($0)" } 复制代码
这样就可以在每段首行添加一个合适的缩进了
接下来就是重点的分页了
第一步, 前期参数准备:
准备好处理完成的 NSAttributedString
, 最好包含各种字体, 颜色, 格式等设置信息, 避免分页视图拿到数据后再次生成 NSAttributedString
, 重复设置内容样式导致的分页不准的情况
准备好文字显示区域大小的参数
第二步, 开始分页:
准备数据:
// 创建 NSLayoutManager, 所有的分页逻辑开端 let layoutManager = NSLayoutManager() // 如果没有给特定部分文字区域设置单独的布局, 可设置此项为 false, 以提高性能 layoutManager.allowsNonContiguousLayout = false // 使用之前准备好的 NSAttributedString 进行初始化 NSTextStorage let textStorage = NSTextStorage(attributedString: string) textStorage.addLayoutManager(layoutManager) // 设定文字显示区域参数 let viewSize: CGSize = CGSize(width: textAreaWidth, height: textAreaHeight) // 设定 textView 的内间距 let textInsets = UIEdgeInsets.zero let textViewFrame = CGRect(x: 0, y: 0, width: viewSize.width, height: viewSize.height) // 开始分页 var glyphRange: Int = 0 var numberOfGlyphs: Int = 0 复制代码
分页循环:
var ranges: [NSRange] = [] repeat { let textContainer = NSTextContainer(size: viewSize) layoutManager.addTextContainer(textContainer) // 不断创建 textView 让 NSLayoutManager 进行内容分页 let textView = UITextView(frame: textViewFrame, textContainer: textContainer) textView.isEditable = false textView.isSelectable = false textView.textContainerInset = textInsets textView.showsVerticalScrollIndicator = false textView.showsHorizontalScrollIndicator = false textView.isScrollEnabled = false // 禁止滑动, 否则计算结果将不再准确 textView.bounces = false textView.bouncesZoom = false // 获取当前分页内容所在位置 let range = layoutManager.glyphRange(for: textContainer) ranges.append(range) // 判定是否分页完成 glyphRange = NSMaxRange(range) numberOfGlyphs = layoutManager.numberOfGlyphs } while glyphRange < numberOfGlyphs - 1 复制代码
至此, 就得到了带有格式的全文 NSAttributedString
, 和分页区域的 ranges
第三步, 显示分页数据
章节内容分页视图中, 将单章的 NSAttributedString
和分到的 range
分配给每一个单页显示视图, 在 UITextView
中直接设置 attributedText
为 attributedString.attributedSubstring(from: range)
UITextView
的设置务必于分页循环时的 UITextView
保持一致
NSLayoutManager
会根据加入的 NSTextContainer
不断分走文字, 直到分完为止, 这时候可以使用 layoutManager.glyphRange(for: textContainer)
获取 NSTextContainer
对应的文字范围 range
, 之后就可以根据这个 range
进行文字分割
改变颜色不需要重新尽心分页操作, 直接操作 UITextView
的 attributedText
和原始 NSAttributedString
就行
let attributed = NSMutableAttributedString(attributedString: textView.attributedText!) attributed.addAttribute(.foregroundColor, value: ChangeColor, range: .init(location: 0, length: attributed.length)) textView.attributedText = NSAttributedString(attributedString: attributed) 复制代码
注意, 方法为 addAttribute
, 而不是 setAttribute
, 后者会导致其他信息被清空
对 UITextView
的 attributedText
和原始 NSAttributedString
的 font
设置为新字体, 再重新进行分页操作, 重新设置单页显示视图即可
UITextView
内间距请通过 textContainerInset
设置间距, 与分页时的参数保持一致, 单独设置 contentInset
不保证显示正确
直接在根视图添加点击手势, 设置代理后, 根据点击区域判断行为
这样可以避免 UIPageViewController
的翻页手势被遮挡
请自主做好手势冲突的处理, 不然就是一片乱
由于分页流程主要在主线程上, 所以被分页的数据最好不要过大, 单章单章分页就刚刚好
每个 NSTextContainer
的 frame 值都是被 NSLayoutManager
粗略计算过的, 与你设置 NSTextContainer
的 size 值略有出入, 有时候大些, 有时候小些, 但误差绝度不会超过一个字符的高度. 所以, 苹果建议我们在设置 UITextView
的时候, 给这个 NSTextContainer
预留一定的高度......
还有字体问题, 因为系统有些字体对中文支持不太好, 可能会对文字的大小计算失误, 请尽量使用以下支持中文的字体, 或其他支持中文的自定义字体:
Heiti SC 黑体-简 Heiti TC 黑体-繁 PingFang TC 平方-简 PingFang HK 平方-繁 PingFang SC 平方-繁 复制代码
可以添加分页中标记, 存在标识时, 下一页上一页代理中返回 nil
具体判断逻辑请根据自身项目调整
可以尝试一下, 内存的飙升绝对酸爽, 我在模拟器上测试, 翻了几页直接飙到 150+ M, 目前的方案在模拟器上 App 整体内存占用最高稳定在 50 M 左右, 真机可以稳定在 20 M 左右
当然, 也有可能是我的方式有错误, 各位可以尝试各种方案, 但分页逻辑万变不离其宗