Swift教程

如何优雅的做一个小说阅读功能

本文主要是介绍如何优雅的做一个小说阅读功能,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目标

  1. 使用 TextKit 快速分页
  2. 使用 UIPageViewController

支持平台

iOS, iPadOS
也许还支持 Mac Calalyst ?

使用语言

Swift

视图结构

|- UIViewController // 根视图, 可添加菜单显示, 手势操作等
    |- UIPageController // 章节视图, 一页对应一章
        | - UIPageController // 章节内容分页视图, 将单章内容进行分页显示
        |   | - UIViewController // 单页显示视图, 对应单页数据
        |   |   |- UITextView // 文字视图
        |   |
        |   | - UIViewController
        |   |   |- UITextView
        |   |
        |   | ...
        |
        | - UIPageController
        |   | - UIViewController
        |   |   |- UITextView
        |   |
        |   | - UIViewController
        |   |   |- UITextView
        |   |
        |   | ...
        |
        | ...
复制代码

章节内容分页视图中, 只要在返回单页显示视图的代理中返回 nil, 即可实现章节内容翻到最后一页时, 继续翻页翻到下一章节的逻辑

分页实现

首先, 一定要先确定好 TextView 的大小与内容间距, 即文字显示区域的大小, 这将严重影响到分页后的数据能不能正常显示

其次, 首行缩进最好用空格代替, 而不是用 NSParagraphStylefirstLineHeadIndent 属性来实现, 否则会出现某段落从中间被分开, 下一页依然被缩进的情况

首行缩进的空格数量可用以下逻辑计算:

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)" }
复制代码

这样就可以在每段首行添加一个合适的缩进了

接下来就是重点的分页了


第一步, 前期参数准备:

  1. 准备好处理完成的 NSAttributedString, 最好包含各种字体, 颜色, 格式等设置信息, 避免分页视图拿到数据后再次生成 NSAttributedString , 重复设置内容样式导致的分页不准的情况

  2. 准备好文字显示区域大小的参数


第二步, 开始分页:
准备数据:

// 创建 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 中直接设置 attributedTextattributedString.attributedSubstring(from: range)

UITextView 的设置务必于分页循环时的 UITextView 保持一致

基本原理

NSLayoutManager 会根据加入的 NSTextContainer 不断分走文字, 直到分完为止, 这时候可以使用 layoutManager.glyphRange(for: textContainer) 获取 NSTextContainer 对应的文字范围 range, 之后就可以根据这个 range 进行文字分割

修改字色, 字体

改变字色

改变颜色不需要重新尽心分页操作, 直接操作 UITextViewattributedText 和原始 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, 后者会导致其他信息被清空

改变字体

UITextViewattributedText 和原始 NSAttributedStringfont 设置为新字体, 再重新进行分页操作, 重新设置单页显示视图即可

注意事项与其他

UITextView 内间距

请通过 textContainerInset 设置间距, 与分页时的参数保持一致, 单独设置 contentInset 不保证显示正确

添加点击区域

直接在根视图添加点击手势, 设置代理后, 根据点击区域判断行为 这样可以避免 UIPageViewController 的翻页手势被遮挡

在 UIPageViewController 中添加 UISlider 等带有活动操作的视图

请自主做好手势冲突的处理, 不然就是一片乱

分页性能

由于分页流程主要在主线程上, 所以被分页的数据最好不要过大, 单章单章分页就刚刚好

分页后文字可能超出显示区域

每个 NSTextContainer 的 frame 值都是被 NSLayoutManager 粗略计算过的, 与你设置 NSTextContainer 的 size 值略有出入, 有时候大些, 有时候小些, 但误差绝度不会超过一个字符的高度. 所以, 苹果建议我们在设置 UITextView 的时候, 给这个 NSTextContainer 预留一定的高度......

还有字体问题, 因为系统有些字体对中文支持不太好, 可能会对文字的大小计算失误, 请尽量使用以下支持中文的字体, 或其他支持中文的自定义字体:

Heiti SC              黑体-简
Heiti TC              黑体-繁
PingFang TC           平方-简
PingFang HK           平方-繁
PingFang SC           平方-繁
复制代码

快速翻页导致未分页完成就翻到下一章

可以添加分页中标记, 存在标识时, 下一页上一页代理中返回 nil

具体判断逻辑请根据自身项目调整

为何不直接使用分页循环中的 UITextView

可以尝试一下, 内存的飙升绝对酸爽, 我在模拟器上测试, 翻了几页直接飙到 150+ M, 目前的方案在模拟器上 App 整体内存占用最高稳定在 50 M 左右, 真机可以稳定在 20 M 左右

当然, 也有可能是我的方式有错误, 各位可以尝试各种方案, 但分页逻辑万变不离其宗

这篇关于如何优雅的做一个小说阅读功能的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!