本文介绍本地 .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,就好了

ChapterBriefModelReadChapterModel 的 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: ReadMarkViewDelegateextension 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 链接