共计 17822 个字符,预计需要花费 45 分钟才能阅读完成。
本文介绍本地 .txt
小说阅读器性能开发的 5 个相干技术点。
网络 .txt 小说开发,则多了下载和缓存两步
一本书有什么,即书的数据结构
一本书有书名,有注释,有目录
手机书架上的书很多,需给书调配一个 id,去除反复
小说用户的常见操作有两种,以后浏览进度记录和书签列表
小说的次要模型 ReadModel
书的两个天然属性:ID 和目录
(一本书有书名,这里与 ID 合并)
书的两个用户操作属性,浏览记录和书签
class ReadModel: NSObject,NSCoding {
/// 小说 ID,书名
let bookID:String
/// 目录,章节列表
// 书的注释,依照章节拆分,保留在 ChapterBriefModel 关联的 ReadChapterModel 中
var chapterListModels = [ChapterBriefModel]()
/// 以后浏览记录
var recordModel:ReadRecordModel?
/// 书签列表
var markModels = [ReadMarkModel]()}
小说的目录模型 ChapterBriefModel
class ChapterBriefModel{
/// 章节 ID
var id: Int!
/// 小说 ID
var bookID:String!
/// 章节名称
var name:String!
}
有了目录,要浏览,须要注释
小说的章节模型
蕴含具体的浏览章节纯文本 content,和用来渲染出现的富文本 fullContent
含有上一章和下一章的 ID,作为一个链表,用于间断浏览
class ReadChapterModel: NSObject,NSCoding {
/// 小说 ID
let bookID: String
/// 章节 ID
let id: Int
/// 上一章 ID
var previousChapterID: Int?
/// 下一章 ID
var nextChapterID: Int?
/// 章节名称
var name:String!
/// 内容
/// 此处 content 是通过排版好且双空格结尾的内容。var content:String!
/// 能够渲染的富文本内容
var fullContent:NSAttributedString!
/// 本章有多少页
var pageCount: Int = 0
/// 分页数据,// 一屏幕内容,对应一个 ReadPageModel
var pageModels = [ReadPageModel]()
/// 内容的排版属性
private var attributes = [NSAttributedString.Key: Any]()}
小说的章节模型 ReadChapterModel 通过 bookID 小说 ID 和 id 章节 ID,
与下面的目录模型 ChapterBriefModel 作关联,
有了 ChapterBriefModel,拿关联信息,去解档,找出 ReadChapterModel
这样的益处是:
一本《三国演义》的 txt,1.8 M, 有 120 章,拆分成 120 个占内存的 ReadChapterModel,
占内存的 ReadChapterModel 须要时解档,不须要就开释,
浏览模型 ReadModel 持有的是,轻量级的目录模型 ChapterBriefModel
小说一屏幕内容,就是一页,一个 ReadPageModel
class ReadPageModel: NSObject,NSCoding {
// MARK: 罕用属性
/// 当前页内容
var content:NSAttributedString!
/// 当前页范畴,//(当前页的第一个字,是第多少个),(当前页有多少字)var pageRange:NSRange!
/// 当前页的页码
var page: Int = 0
// MARK: 滚动模式相干
// 滚动模式的排版
/// 依据结尾类型返回结尾高度
var headTypeHeight:CGFloat = 0
/// 以后内容 Size
var contentSize = CGSize.zero
/// 以后内容头部类型
private
var headTypeIndex: Int = 0
/// 以后内容头部类型
var headType: PageHeadType? {
set{
if let n = newValue{headTypeIndex = n.rawValue}
}
get{PageHeadType(rawValue: headTypeIndex)
}
}
}
一本书的数据结构确立后,进入性能开发
1,根底出现:
网上下载了一本《三国演义》,制作一个根本的浏览界面
.txt 小说 -> 小说代码模型 -> 用视图把小说出现进去
1.1 模型解析
1.1.1 把资源门路,转化为注释
对文本编码
class func encode(url:URL) -> String {
var content = ""
if url.absoluteString.isEmpty {return content}
// utf8
content = encode(path: url, encoding: String.Encoding.utf8.rawValue)
// 进制编码
if content.isEmpty {content = encode(path: url, encoding: 0x80000632) }
if content.isEmpty {content = encode(path: url, encoding: 0x80000631) }
if content.isEmpty {content = ""}
return content
}
class func encode(path url:URL, encoding:UInt) ->String {
do{return try NSString(contentsOf: url, encoding: encoding) as String
}
catch{return ""}
}
1.1.2 解析出所有的章节目录,不含注释的 ChapterBriefModel,含注释的 ReadChapterModel
上面的代码,分为两局部,一个正则,一个 for 循环
把注释作为一个字符串,正则拆分出所有的章节,
for 循环中,把拆除来的章节,映射为 ChapterBriefModel 和 ReadChapterModel
ReadChapterModel 归档长久化,调用时再解档
/// 解析整本小说
/// - Parameters: - bookID: 小说 ID - content: 小说内容
/// - Returns: 章节列表
private class func parser(segments bookID:String, content:String) ->[ChapterBriefModel] {
// 章节列表
var chapterListModels = [ChapterBriefModel]()
// 正则
let parten = "第 [0- 9 一二三四五六七八九十百千]*[章回].*"
// 排版
let content = ReadParserIMP.contentTypesetting(content: content)
// 正则匹配后果
var results = [NSTextCheckingResult]()
// 开始匹配
do{let regularExpression = try NSRegularExpression(pattern: parten, options: .caseInsensitive)
results = regularExpression.matches(in: content, options: .reportCompletion, range: NSRange(location: 0, length: content.count))
}catch{return chapterListModels}
// 解析匹配后果
guard results.isEmpty == false else {
// ....
return // ...
}
// 章节数量
let count = results.count
// 记录最初一个 Range
var lastRange:NSRange!
// 记录最初一个章节对象 C
var lastChapterModel:ReadChapterModel?
// 有前言
var isHavePreface = true
// 遍历
for i in 0...count {
// 章节数量剖析:
// count + 1 = 匹配到的章节数量 + 最初一个章节
// 1 + count + 1 = 第一章后面的前言内容 + 匹配到的章节数量 + 最初一个章节
Log("章节总数: \(count + 1) 以后正在解析: \(i + 1)")
var range = NSMakeRange(0, 0)
var location = 0
if i < count {range = results[i].range
location = range.location
}
// 章节内容
let chapterModel = ReadChapterModel(id: i + isHavePreface.val, in: bookID)
switch i {
case 0:
// 前言
// 章节名
chapterModel.name = "开始"
// 内容
chapterModel.content = content.substring(NSMakeRange(0, location))
// 记录
lastRange = range
// 没有内容则不须要增加列表
if chapterModel.content.isEmpty {
isHavePreface = false
continue
}
case count:
// 结尾
// 章节名
chapterModel.name = content.substring(lastRange)
// 内容 (不蕴含章节名)
chapterModel.content = content.substring(NSMakeRange(lastRange.rhs, content.count - lastRange.rhs))
default:
// 两头章节
// 章节名
chapterModel.name = content.substring(lastRange)
// 内容 (不蕴含章节名)
chapterModel.content = content.substring(NSMakeRange(lastRange.rhs, location - lastRange.rhs))
}
// 章节结尾双空格 + 章节纯内容
chapterModel.content = TypeSetting.readSpace + chapterModel.content.removeSEHeadAndTail
// 设置上一个章节 ID
chapterModel.previousChapterID = lastChapterModel?.id ?? nil
// 设置下一个章节 ID
if i == (count - 1) { // 最初一个章节了
chapterModel.nextChapterID = nil
}
else{lastChapterModel?.nextChapterID = chapterModel.id}
// 保留
chapterModel.persist()
lastChapterModel?.persist()
// 记录
lastRange = range
lastChapterModel = chapterModel
// 通过章节内容生成章节列表
chapterListModels.append(chapterModel.chapterList)
}
// 返回
return chapterListModels
}
1.1.3 产生浏览模型
// 浏览模型
let readModel = ReadModel.model(bookID: bookID)
// 记录章节列表
readModel.chapterListModels = chapterListModels
// 设置第一个章节为浏览记录
readModel.recordModel?.modify(chapterID: readModel.chapterListModels.first!.id, toPage: 0)
拿到浏览模型,展现进去,就能够看书了
1.2 视图出现
- 浏览文本视图 ReadView ->
- 浏览控制器,增加状态栏 ReadViewController ->
- 浏览的主控制器(带菜单性能的)->
- 主控制器的,翻页模式解决
1.2.1, 浏览文本视图 ReadView
制订一页的模型 ReadPageModel,产生一帧文本 CTFrame,文本绘制到界面上,ok
class ReadView: UIView {/// 当前页模型 ( 应用 contentSize 绘制)
var pagingModel:ReadPageModel! {
didSet{frameRef = CoreText.GetFrameRef(attrString: pagingModel.showContent, rect: CGRect(origin: CGPoint.zero, size: pagingModel.contentSize))
}
}
/// CTFrame
var frameRef:CTFrame? {
didSet{if frameRef != nil { setNeedsDisplay() }
}
}
/// 绘制
override func draw(_ rect: CGRect) {guard let frame = frameRef, let ctx = UIGraphicsGetCurrentContext() else {return}
ctx.textMatrix = CGAffineTransform.identity
ctx.translateBy(x: 0, y: bounds.size.height)
ctx.scaleBy(x: 1.0, y: -1.0)
CTFrameDraw(frame, ctx)
}
}
1.2.2, 浏览控制器,增加状态栏 ReadViewController
- 增加顶部状态栏,顶部有书名和章节名
- 增加底部状态栏,底部有以后的进度
- 浏览视图展现。是首页,展现封面。不是,就展现注释
class ReadViewController: ViewController {
// 须要两个对象,当前页浏览记录 和 浏览对象
/// 当前页浏览记录对象
var recordModelBasic:ReadRecordModel!
/// 浏览对象 (用于显示书名以及书籍首页显示书籍信息)
weak var readModel:ReadModel!
/// 顶部状态栏
var topView:ReadViewStatusTopView!
/// 底部状态栏
var bottomView:ReadViewStatusBottomView!
/// 浏览视图
private var readView:ReadView!
/// 书籍首页视图,封面
private var homeView:ReadHomeView!
}
1.2.3, 浏览的主控制器(带菜单性能的)
- 增加左侧弹窗,章节列表, 和书签
- 增加设置菜单
菜单包含:
顶部栏,书签按钮和返回按钮
底部栏,上一章按钮、下一章按钮和进度拖动,目录入口和设置入口
设置栏,管制字体大小和品种,管制翻页形式,管制进度展现形式
- 增加浏览容器视图
浏览容器视图,下面是管制翻页形式的控制器的视图
管制翻页形式的控制器,治理上一步的浏览控制器 ReadViewController
class ReadController: ViewController{
// MARK: 数据相干
/// 浏览对象
let readModel:ReadModel
// MARK: UI 相干
/// 浏览容器视图
var contentView = ReadContentView(frame: CGRect(x: 0, y: 0, width: ScreenWidth, height: ScreenHeight))
/// 左侧弹窗:章节列表, 和书签
var leftView = ReadLeftView(frame: CGRect(x: -READ_LEFT_VIEW_WIDTH, y: 0, width: READ_LEFT_VIEW_WIDTH, height: ScreenHeight))
/// 底部设置菜单
lazy var readMenu = ReadMenu(vc: self, delegate: self)
// 管制翻页形式的控制器:/// 翻页控制器 (仿真)
var pageViewController:UIPageViewController!
/// 翻页控制器 (滚动)
var scrollController:ReadViewScrollController!
/// 翻页控制器 (无成果, 笼罩)
var coverController:CoverController!
/// 非滚动模式时, 以后显示 ReadViewController
var currentDisplayController:ReadViewController?
}
1.2.4, 翻页模式解决
翻页模式,有仿真、平移和滚动
这里以仿真为例子:
仿真的成果,应用 UIPageViewController
- 先增加 UIPageViewController 的视图,到浏览容器视图 contentView 下面
func creatPageController(displayController:ReadViewController? = nil) {
guard let displayCtrl = displayController else {return}
// 创立
let options = [UIPageViewController.OptionsKey.spineLocation : NSNumber(value: UIPageViewController.SpineLocation.min.rawValue)]
pageViewController = UIPageViewController(transitionStyle: .pageCurl,navigationOrientation: .horizontal,options: options)
pageViewController.delegate = self
pageViewController.dataSource = self
// 翻页背部带文字效果
pageViewController.isDoubleSided = true
contentView.insertSubview(pageViewController.view, at: 0)
pageViewController.view.backgroundColor = UIColor.clear
pageViewController.view.frame = contentView.bounds
pageViewController.setViewControllers([displayCtrl], direction: .forward, animated: false, completion: nil)
}
- 提供分页控制器的内容,即浏览内容
以下是获取下一页的代码,
获取上一页的,相似
/// 获取下一页
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
tempNumber += 1
// 获取当前页浏览记录
var recordModel:ReadRecordModel? = (viewController as? ReadViewController)?.recordModelBasic
// 如果没有则从反面页面获取
if recordModel == nil {recordModel = (viewController as? ReadViewBGController)?.recordModel
}
if abs(tempNumber) % 2 == 0 { // 反面
return getBackgroundController(recordModel: recordModel)
}
else{
// 内容
recordModel = getBelowReadRecordModel(recordModel: recordModel)
return getReadController(recordModel: recordModel)
}
}
这样,.txt 的小说,可读一下了
2,计算页码
一个章节有几页,是怎么计算出来的?
先拿着一个章节的富文本,和显示区域,计算出书页的范畴
通常显示区域,是放不满一章的。
显示区域先放一页,失去这一页的开始范畴和长度,对应一个 ReadPageModel
显示区域再放下一页 …
/// 取得内容分页列表
/// - Parameters: - attrString: 内容 - rect: 显示范畴
/// - Returns: 内容分页列表
class func pagingRanges(attrString:NSAttributedString, rect:CGRect) ->[NSRange] {var rangeArray = [NSRange]()
let framesetter = CTFramesetterCreateWithAttributedString(attrString as CFAttributedString)
let path = CGPath(rect: rect, transform: nil)
var range = CFRangeMake(0, 0)
var rangeOffset = 0
repeat{let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(rangeOffset, 0), path, nil)
range = CTFrameGetVisibleStringRange(frame)
rangeArray.append(NSMakeRange(rangeOffset, range.length))
rangeOffset += range.length
}while(range.location + range.length < attrString.length)
return rangeArray
}
拿上一步计算出来的范畴,创立该章节每一页的模型 ReadPageModel
/// 内容分页
/// - Parameters: - attrString: 内容 - rect: 显示范畴
/// - isFirstChapter: 是否为本文章第一个展现章节, 如果是则退出书籍首页
/// - Returns: 内容分页列表
class func pageing(attrString:NSAttributedString, rect:CGRect, isFirstChapter:Bool = false) ->[ReadPageModel] {var pageModels = [ReadPageModel]()
if isFirstChapter { // 第一页为书籍页面
let pageModel = ReadPageModel()
pageModel.pageRange = NSMakeRange(TypeSetting.readBookHomePage, 1)
pageModel.contentSize = READ_VIEW_RECT.size
pageModels.append(pageModel)
}
let ranges = CoreText.pagingRanges(attrString: attrString, rect: rect)
if !ranges.isEmpty {
let count = ranges.count
for i in 0..<count {let range = ranges[i]
let pageModel = ReadPageModel()
let content = attrString.attributedSubstring(from: range)
pageModel.pageRange = range
pageModel.content = content
pageModel.page = i
// ...
// 内容 Size (滚动模式 || 长按菜单)
let maxW = READ_VIEW_RECT.width
pageModel.contentSize = CGSize(width: maxW, height: CoreText.GetAttrStringHeight(attrString: content, maxW: maxW))
// ...
pageModels.append(pageModel)
}
}
return pageModels
}
该章节 ReadPageModel
的数目,就是该章节有几页
2.1 翻页
获取下一页的代码
翻一页,就是以后的 ReadRecordModel,翻到下一页,
交给浏览控制器去出现,ReadViewController 的子类 ReadLongPressViewController
规范的模型更新,刷新视图
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
// ...
// 内容
recordModel = recordModel?.getBelowReadRecordModel
return getReadController(recordModel: recordModel)
}
/// 获取指定浏览记录浏览页
func getReadController(recordModel:ReadRecordModel!) ->ReadViewController? {
if recordModel != nil {
// 须要返回反对长按的控制器
let controller = ReadLongPressViewController()
controller.recordModelBasic = recordModel
controller.readModel = readModel
return controller
}
return nil
}
浏览记录模型:
ReadRecordModel,次要是三个属性,
一本小说,绑定一个进度,须要小说 ID
以后看到那一章,有一个章节的模型 ReadChapterModel
以后这一章,看到第几页了,有一个页码 page,
能够计算出,
以后浏览到的这一屏的,页面模型 ReadPageModel
和以后浏览到的这一屏的富文本 contentAttributedString,用来渲染
class ReadRecordModel: NSObject,NSCoding {
/// 小说 ID
var bookID:String!
/// 以后记录的浏览章节
var chapterModel:ReadChapterModel!
/// 浏览到的页码
var page:Int = 0
/// 以后记录分页模型
var pageModel:ReadPageModel{chapterModel.pageModels[page]
}
/// 以后记录页码富文本
var contentAttributedString:NSAttributedString {chapterModel.contentAttributedString(page: page)
}
ReadRecordModel,翻页的计算逻辑:
本章内,页码 + 1,就好了,page 解决下
本章最初一页了,换下一章
本章到了最初一章,最初一页了,就翻不动了
/// 获取以后记录下一页浏览记录
var getBelowReadRecordModel: ReadRecordModel?{
// ...
// 复制
let recordModel = copyModel
// 书籍 ID
// 章节 ID
guard let bookID = recordModel.bookID, let chapterID = recordModel.chapterModel.nextChapterID else{return nil}
// 最初一章 最初一页
if recordModel.isLastChapter, recordModel.isLastPage {Log("曾经是最初一页了")
return nil
}
// 最初一页
if recordModel.isLastPage {
// 查看是否存在章节内容
if ReadChapterModel.isExist(bookID: bookID, chapterID: chapterID){
// 批改浏览记录
recordModel.modify(chapterID: chapterID, toPage: 0, isSave: false)
}
}else{recordModel.nextPage()
}
return recordModel
}
3,目录
目录展现,比较简单
把上文解析进去的目录模型 ChapterBriefModel
,用一个列表展现就好了
滚动到浏览记录
譬如,以后浏览到第 50 章了,关上目录,显示第一章,不太好。
须要滚动到,浏览记录对应的章节
以后浏览进度,应用 recordModel 追踪,
从目录 ChapterBriefModel
列表中,找出 recordModel 的章节模型的 id,就好了
ChapterBriefModel
和 ReadChapterModel
的 id 是一一对应的
/// 滚动到浏览记录
func scrollRecord() {
if let read = readModel, let record = read.recordModel {tableView.reloadData()
if let chapterListModel = (read.chapterListModels as NSArray).filtered(using: NSPredicate(format: "id == %ld", record.chapterModel.id)).first as? ChapterBriefModel{tableView.scrollToRow(at: read.chapterListModels.firstIndex(of: chapterListModel)!.ip, at: .middle, animated: false)
}
}
}
4,书签
从读到的地位,增加书签。
书签列表中,用书签,返回读到的地位
书签的数据结构
一个书签,绑定具体的小说,与该小说的某个章节
书签,最好能展现一些上次浏览的信息,content
要从书签,返回到浏览到的中央,须要一个地位 location
class ReadMarkModel: NSObject,NSCoding {
/// 小说 ID
var bookID:String!
/// 章节 ID
var chapterID: Int!
/// 章节名称
var name:String!
// 内容,// 对应以后屏幕浏览模型 ReadPageModel 的内容
var content:String!
/// 工夫戳
var time = NSNumber(value: 0)
/// 地位
// 对应以后屏幕浏览模型 ReadPageModel 的范畴的开始点
var location: Int = 0
4.1 创立书签
从读到的地位,增加书签。
书签的展现,采纳逆序。最新的,摆在前面,也就是最近增加的。
/// 增加书签, 默认应用以后浏览记录!
func insetMark(recordModel:ReadRecordModel? = nil) {let recordModel = (recordModel ?? self.recordModel)!
let markModel = ReadMarkModel()
markModel.bookID = recordModel.bookID
markModel.chapterID = recordModel.chapterModel.id
if recordModel.pageModel.isHomePage {markModel.name = "( 无章节名)"
markModel.content = bookID
}else{
markModel.name = recordModel.chapterModel.name
// 以后屏幕浏览模型 ReadPageModel 的内容,略微解决了下
markModel.content = recordModel.contentString.removeSEHeadAndTail.removeEnterAll
}
markModel.time = NSNumber(value: Timer1970())
// location,对应以后屏幕浏览模型 ReadPageModel 的范畴的开始点
markModel.location = recordModel.locationFirst
if markModels.isEmpty {markModels.append(markModel)
}else{markModels.insert(markModel, at: 0)
}
// ...
}
4.2 从书签列表,抉择书签,返回读到的中央
点击具体的书签,先解决下 UI,
再拿着章节 ID 和该章节的地位,去跳转上次读到的中央
// MARK: ReadMarkViewDelegate
extension ReadController: ReadMarkViewDelegate{
/// 书签列表选中书签
func markViewClickMark(markView: ReadMarkView, markModel: ReadMarkModel) {showLeftView(isShow: false)
contentView.showCover(isShow: false)
goToChapter(chapterID: markModel.chapterID, coordinate: markModel.location)
}
}
跳转上次读到的中央,就是拿记录浏览地位的 ReadRecordModel,
更新他的章节模型 ReadChapterModel,和浏览到的地位 page
模型更新,刷新 UI 界面,就是跳转过来了
/// 跳转指定章节 (指定坐标)
func goToChapter(chapterID: Int, coordinate location: Int) {
// 复制浏览记录
let recordModel = readModel.recordModel?.copyModel
// 书籍 ID
guard let bookID = recordModel?.bookID else{return}
// 查看是否存在章节内容
// 存在
if ReadChapterModel.isExist(bookID: bookID, chapterID: chapterID){
// 坐标定位
recordModel?.modifyLoc(chapterID: chapterID, location: location, isSave: false)
// 浏览浏览记录
if let record = recordModel{update(read: record)
}
// 展现浏览
creatPageController(displayController: getReadController(recordModel: recordModel))
}
}
ReadRecordModel,更新内容
刷新章节目录 ReadChapterModel,比较简单,拿书的 id 和章节 id, 去创立新的
该章节中,浏览到第几页,须要在 ReadChapterModel 中计算下
class ReadRecordModel{
/// 批改浏览记录为指定章节地位
func modifyLoc(chapterID: Int, location: Int, isSave:Bool = true) {if ReadChapterModel.isExist(bookID: bookID, chapterID: chapterID) {chapterModel = ReadChapterModel(id: chapterID, in: bookID).real
page = chapterModel.page(location: location)
if isSave {save() }
}
}
}
ReadChapterModel 中计算,该章节浏览到第几页
拿着地位,在章节模型的书页模型列表中比拟范畴,得出
class ReadChapterModel{
// 获取存在指定坐标的页码
func page(location: Int?) -> Int {
let count = pageModels.count
guard let loc = location else {return 0}
for i in 0..<count {let range = pageModels[i].pageRange!
if loc < range.rhs {return i}
}
return 0
}
}
总结上文,性能次要是,各种模型之间的对应关系,转换来,转换去,跟数据库操作很像
5,滚动条,调进度
调进度分两种,
- 全文滚动,拉章节
全文百分比展现进度,滚动是在全文范畴内的;
- 章节滚动,拉书页
以后章节展现进度,按页码,滚动是在以后章节范畴内的
5.1 全文的进度展现与调节
全文百分比展现进度
拿着以后的浏览记录,去计算
- 是尾页,则很好计算
- 不是尾页,
先算出以后章初始地位的进度,chapterIndex/chapterCount
再加上当前页,在以后章的进度,(locationFirst / fullContentLength)/chapterCount
算的精度个别,把每一章的长度,等价了
extension ReadModel{
/// 计算总进度
func progress(readTotal recordModel:ReadRecordModel!) ->Float {
// 以后浏览进度
var progress:Float = 0
// 长期查看
if recordModel == nil {return progress}
if recordModel.isLastChapter, recordModel.isLastPage { // 最初一章最初一页
// 取得以后浏览进度
progress = 1.0
}else{
// 以后章节在所有章节列表中的地位
let chapterIndex = Float(recordModel.chapterModel.priority)
// 章节总数量
let chapterCount = Float(chapterListModels.count)
// 浏览记录首地位
let locationFirst = Float(recordModel.locationFirst)
// 浏览记录内容长度
let fullContentLength = Float(recordModel.chapterModel.fullContent.length)
// 取得以后浏览进度
progress = chapterIndex/chapterCount + (locationFirst / fullContentLength)/chapterCount
}
// 返回
return progress
}
}
滚动是在全文范畴内,只能拉到某一章的结尾
滚动条代理中,找到滚动的范畴,
用该范畴,找出目录列表中,对应的那一章,
跳过去,就好了
- 拉到底,就跳尾页
- 没拉到底,就跳到那一章的结尾
/// 进度显示将要暗藏
func sliderWillHidePopUpView(_ slider: ASValueTrackingSlider!) {
// 有浏览数据
let readModel = readMenu.vc.readModel
// 有浏览记录以及章节数据
if readModel.recordModel?.chapterModel != nil{
// 总章节个数
let count = (readModel.chapterListModels.count - 1)
// 取得以后进度的章节索引
let index = Int(Float(count) * slider.value)
// 取得章节列表模型
let chapterListModel = readModel.chapterListModels[index]
// 页码
let toPage = (index == count) ? ReadingConst.lastPage : 0
// 传递
readMenu?.delegate?.readMenuDraggingProgress(readMenu: readMenu, toChapterID: chapterListModel.id, toPage: toPage)
}
}
上面两个办法,
就是模型更新,刷新界面
模型更新,就是更新以后浏览记录模型 ReadRecordModel 的地位
/// 拖拽章节进度
func readMenuDraggingProgress(readMenu: ReadMenu, toChapterID: Int, toPage: Int) {
// 不是以后浏览记录章节
if toChapterID != readModel.recordModel?.chapterModel.id{goToChapter(chapterID: toChapterID, to: toPage)
// 查看以后内容是否蕴含书签
readMenu.topView.checkForMark()}
}
/// 跳转指定章节的指定页面
func goToChapter(chapterID: Int, to page: Int = 0) {
// 复制浏览记录
let recordModel = readModel.recordModel?.copyModel
// 书籍 ID
guard let bookID = recordModel?.bookID else{return}
// 查看是否存在章节内容
// 存在
if ReadChapterModel.isExist(bookID: bookID, chapterID: chapterID){
// 分页定位
recordModel?.modify(chapterID: chapterID, toPage: page, isSave: false)
// 浏览浏览记录
if let record = recordModel{update(read: record)
}
// 展现浏览
creatPageController(displayController: getReadController(recordModel: recordModel))
}
}
5.2 以后章节的进度展现与调节
分页进度,
进度就靠 以后浏览记录模型 ReadRecordModel
/// 刷新浏览进度显示
private func reloadProgress() {
// 分页进度
if let record = vc.readModel.recordModel{bottomView.progress.text = "\(record.page + 1)/\(record.chapterModel!.pageCount)"
}
// 显示进度
}
滚动是在以后章节范畴内,拉到某一书页
返回某一书页,就是更新以后浏览记录模型 ReadRecordModel 的地位,
刷新界面
/// 进度显示将要暗藏
func sliderWillHidePopUpView(_ slider: ASValueTrackingSlider!) {
// 分页进度
readMenu?.delegate?.readMenuDraggingProgress(readMenu: readMenu, toPage: Int(slider.value - 1))
}
以后章节范畴内,更新以后浏览记录模型 ReadRecordModel 的页码,
比较简单
/// 拖拽浏览记录
func readMenuDraggingProgress(readMenu: ReadMenu, toPage: Int) {
if readModel.recordModel?.page != toPage{
readModel.recordModel?.page = toPage
creatPageController(displayController: getCurrentReadViewController())
// 查看以后内容是否蕴含书签
readMenu.topView.checkForMark()}
}
GitHub 链接