关于ios:txt-小说阅读器指南-功能开发的-5-个老套路

6次阅读

共计 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,就好了

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: 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 链接

正文完
 0