• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Swift拆分小说阅读器功能,内部实现

武飞扬头像
Landen2011
帮助1

  公司项目结束了,公司估计也快黄了,年底事少,也给了我不少时间来维护博客。

  公司的项目是一个类似于简书的创作平台,涵盖写作、小说、插画内容。

  本期主要先下小说阅读部分,UI样式仿照的是微信读书样式,因之前也写过小说阅读器,但是代码并没有解耦,这次彻彻底底做一次大改动。

   小说用户的常见操作:当前阅读进入记录和书签列表,因公司项目的结构问题,目前新项目并没有做项目进度记录和书签保存功能,以后有优化时候,再补充相关内容。先看下小说的结构。

学新通

  小说的主要模型ReadModel

  小说章节模型

  1.  
    class JFChapterModel: NSObject {
  2.  
     
  3.  
    var title: String?
  4.  
    var path: String?
  5.  
    var chapterIndex: Int = 1
  6.  
    }

  小说页面Model,一个页面,就是一个Model

  1.  
    class JFPageModel: NSObject {
  2.  
     
  3.  
    var attributedString: NSAttributedString?
  4.  
    var range: NSRange?
  5.  
    var pageIndex: Int = 1
  6.  
     
  7.  
    }

  一本书的数据结构确立后,进入功能开发

  1、模型解析

  1、把资源路径转化为正文,解析出所有的章节目录,把正文作为一个字符串,正则拆分出所有的章节,映射为ChapterModel

  首先正则获取章节目录

  1.  
    func doTitleMatchWith(content: String) -> [NSTextCheckingResult] {
  2.  
    let pattern = "第[ ]*[0-9一二三四五六七八九十百千]*[ ]*[章回].*"
  3.  
    let regExp = try! NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options.caseInsensitive)
  4.  
    let results = regExp.matches(in: content, options: .reportCompletion, range: NSMakeRange(0, content.count))
  5.  
    return results
  6.  
    }

  1.  
    let content = path
  2.  
    var models = Array<JFChapterModel>()
  3.  
    var titles = Array<String>()
  4.  
    DispatchQueue.global().async {
  5.  
    let document = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).first
  6.  
    let fileName = name
  7.  
    let bookPath = document! "/\(String(fileName))"
  8.  
    if FileManager.default.fileExists(atPath: bookPath) == false {
  9.  
    try? FileManager.default.createDirectory(atPath: bookPath, withIntermediateDirectories: true, attributes: nil)
  10.  
    }
  11.  
     
  12.  
    let results = self.doTitleMatchWith(content: content)
  13.  
    if results.count == 0 {
  14.  
    let model = JFChapterModel()
  15.  
    model.chapterIndex = 1
  16.  
    model.path = path
  17.  
    completeHandler([], [model])
  18.  
    }else {
  19.  
    var endIndex = content.startIndex
  20.  
     
  21.  
    for (index, result) in results.enumerated() {
  22.  
    let startIndex = content.index(content.startIndex, offsetBy: result.range.location)
  23.  
    endIndex = content.index(startIndex, offsetBy: result.range.length)
  24.  
     
  25.  
    let currentTitle = String(content[startIndex...endIndex])
  26.  
    titles.append(currentTitle)
  27.  
    let chapterPath = bookPath "/chapter" String(index 1) ".txt"
  28.  
    let model = JFChapterModel()
  29.  
    model.chapterIndex = index 1
  30.  
    model.title = currentTitle
  31.  
    model.path = chapterPath
  32.  
    models.append(model)
  33.  
     
  34.  
    if FileManager.default.fileExists(atPath: chapterPath) {
  35.  
    continue
  36.  
    }
  37.  
    var endLoaction = 0
  38.  
    if index == results.count - 1 {
  39.  
    endLoaction = content.count - 1
  40.  
    }else {
  41.  
    endLoaction = results[index 1].range.location - 1
  42.  
    }
  43.  
    let startLocation = content.index(content.startIndex, offsetBy: result.range.location)
  44.  
    let subString = String(content[startLocation...content.index(content.startIndex, offsetBy: endLoaction)])
  45.  
    try! subString.write(toFile: chapterPath, atomically: true, encoding: String.Encoding.utf8)
  46.  
     
  47.  
    }
  48.  
     
  49.  
    DispatchQueue.main.async {
  50.  
    completeHandler(titles, models)
  51.  
    }
  52.  
    }
  53.  
    }

学新通

  拿到阅读模型后,展示出来,就可以看书了。

  2、翻页模式处理

  翻页模式,有仿真、平移和滚动

  这里以仿真为例子:

  仿真的效果,使用 UIPageViewController

  先添加 UIPageViewController 的视图,到阅读容器视图 contentView 上面

  1.  
    private func loadPageViewController() -> Void {
  2.  
     
  3.  
    self.clearReaderViewIfNeed()
  4.  
    let transtionStyle: UIPageViewController.TransitionStyle = (self.config.scrollType == .curl) ? .pageCurl : .scroll
  5.  
    self.pageVC = JFContainerPageViewController(transitionStyle: transtionStyle, navigationOrientation: .horizontal, options: nil)
  6.  
    self.pageVC?.dataSource = self
  7.  
    self.pageVC?.delegate = self
  8.  
    self.pageVC?.view.backgroundColor = UIColor.clear
  9.  
     
  10.  
    // 翻页背部带文字效果
  11.  
    self.pageVC?.isDoubleSided = (self.config.scrollType == .curl) ? true : false
  12.  
     
  13.  
    self.addChild(self.pageVC!)
  14.  
    self.view.addSubview((self.pageVC?.view)!)
  15.  
    self.pageVC?.didMove(toParent: self)
  16.  
    }

学新通
  • 提供分页控制器的内容,即阅读内容

  以下是获取下一页的代码,

  获取上一页的,类似

  1.  
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
  2.  
    print("向后翻页 -------1")
  3.  
    struct LastPage {
  4.  
    static var arrived = false
  5.  
    }
  6.  
    let nextIndex: Int
  7.  
    let pageArray = self.pageArrayFromCache(chapterIndex: currentChapterIndex)
  8.  
    if viewController is JFPageViewController {
  9.  
    let page = viewController as! DUAPageViewController
  10.  
    nextIndex = page.index 1
  11.  
    if nextIndex == pageArray.count {
  12.  
    LastPage.arrived = true
  13.  
    }
  14.  
    let backPage = JFBackViewController()
  15.  
    backPage.grabViewController(viewController: page)
  16.  
    return backPage
  17.  
    }
  18.  
    if LastPage.arrived {
  19.  
    LastPage.arrived = false
  20.  
    if currentChapterIndex 1 > totalChapterModels.count {
  21.  
    return nil
  22.  
    }
  23.  
    pageVC?.willStepIntoNextChapter = true
  24.  
    self.requestChapterWith(index: currentChapterIndex 1)
  25.  
    let nextPage = self.getPageVCWith(pageIndex: 0, chapterIndex: currentChapterIndex 1)
  26.  
    /// 需要的页面并没有准备好,此时出现页面饥饿
  27.  
    if nextPage == nil {
  28.  
    self.postReaderStateNotification(state: .busy)
  29.  
    pageHunger = true
  30.  
    }
  31.  
    return nextPage
  32.  
    }
  33.  
    let back = viewController as! JFBackViewController
  34.  
    return self.getPageVCWith(pageIndex: back.index 1, chapterIndex: back.chapterBelong)
  35.  
    }

学新通

  3、计算页码

  一个章节有几页,是怎么计算出来的?

  先拿着一个章节的富文本,和显示区域,计算出书页的范围

  通常显示区域,是放不满一章的。

  显示区域先放一页,得到这一页的开始范围和长度,对应一个 ReadPageModel

  显示区域再放下一页 ...

  1.  
    let layouter = JFCoreTextLayouter.init(attributedString: attrString)
  2.  
    let rect = CGRect(x: config.contentFrame.origin.x, y: config.contentFrame.origin.y, width: config.contentFrame.size.width, height: config.contentFrame.size.height - 5)
  3.  
    var frame = layouter?.layoutFrame(with: rect, range: NSRange(location: 0, length: attrString.length))
  4.  
     
  5.  
    var pageVisibleRange = frame?.visibleStringRange()
  6.  
    var rangeOffset = pageVisibleRange!.location pageVisibleRange!.length

  拿上一步计算出来的范围,创建该章节每一页的模型 ReadPageModel

  1.  
    while rangeOffset <= attrString.length && rangeOffset != 0 {
  2.  
    let pageModel = DUAPageModel.init()
  3.  
    pageModel.attributedString = attrString.attributedSubstring(from: pageVisibleRange!)
  4.  
    pageModel.range = pageVisibleRange
  5.  
    pageModel.pageIndex = count - 1
  6.  
     
  7.  
    frame = layouter?.layoutFrame(with: rect, range: NSRange(location: rangeOffset, length: attrString.length - rangeOffset))
  8.  
    pageVisibleRange = frame?.visibleStringRange()
  9.  
    if pageVisibleRange == nil {
  10.  
    rangeOffset = 0
  11.  
    }else {
  12.  
    rangeOffset = pageVisibleRange!.location pageVisibleRange!.length
  13.  
    }
  14.  
     
  15.  
    let completed = (rangeOffset <= attrString.length && rangeOffset != 0) ? false : true
  16.  
    completeHandler(count, pageModel, completed)
  17.  
    count = 1
  18.  
    }

学新通

  4、翻页

  获取下一页的代码

  翻一页,就是当前的 RecordModel , 翻到下一页,

  交给阅读控制器去呈现, ReadViewController 的子类 ReadLongPressViewController

  标准的模型更新,刷新视图
  1.  
    func setViewController(viewController: UIViewController, direction: translationControllerNavigationDirection, animated: Bool, completionHandler: ((Bool) -> Void)?) -> Void {
  2.  
    if animated == false {
  3.  
    for controller in self.children {
  4.  
    self.removeController(controller: controller)
  5.  
    }
  6.  
    self.addController(controller: viewController)
  7.  
    if completionHandler != nil {
  8.  
    completionHandler!(true)
  9.  
    }
  10.  
    }else {
  11.  
    let oldController = self.children.first
  12.  
    self.addController(controller: viewController)
  13.  
     
  14.  
    var newVCEndTransform: CGAffineTransform
  15.  
    var oldVCEndTransform: CGAffineTransform
  16.  
    viewController.view.transform = .identity
  17.  
    if direction == .left {
  18.  
    viewController.view.transform = CGAffineTransform(translationX: screenWidth, y: 0)
  19.  
    newVCEndTransform = .identity
  20.  
    oldController?.view.transform = .identity
  21.  
    oldVCEndTransform = CGAffineTransform(translationX: -screenWidth, y: 0)
  22.  
    }else {
  23.  
    viewController.view.transform = CGAffineTransform(translationX: -screenWidth, y: 0)
  24.  
    newVCEndTransform = .identity
  25.  
    oldController?.view.transform = .identity
  26.  
    oldVCEndTransform = CGAffineTransform(translationX: screenWidth, y: 0)
  27.  
    }
  28.  
     
  29.  
    UIView.animate(withDuration: animationDuration, animations: {
  30.  
    oldController?.view.transform = oldVCEndTransform
  31.  
    viewController.view.transform = newVCEndTransform
  32.  
    }, completion: { (complete) in
  33.  
    if complete {
  34.  
    self.removeController(controller: oldController!)
  35.  
    }
  36.  
    if completionHandler != nil {
  37.  
    completionHandler!(complete)
  38.  
    }
  39.  
    })
  40.  
    }
  41.  
    }

学新通

  //如果到了最后一章、最后一页时,就翻不动了

  1.  
    self.postReaderStateNotification(state: .ready)
  2.  
    if pageHunger {
  3.  
    pageHunger = false
  4.  
    if pageVC != nil {
  5.  
    self.loadPage(pageIndex: currentPageIndex)
  6.  
    }
  7.  
    if tableView != nil {
  8.  
    if currentPageIndex == 0 && tableView?.scrollDirection == .up {
  9.  
    self.requestLastChapterForTableView()
  10.  
    }
  11.  
    if currentPageIndex == self.pageArrayFromCache(chapterIndex: currentChapterIndex).count - 1 && tableView?.scrollDirection == .down {
  12.  
    self.requestNextChapterForTableView()
  13.  
    }
  14.  
    }
  15.  
    }
  16.  
     
  17.  
    if firstIntoReader {
  18.  
    firstIntoReader = false
  19.  
    currentPageIndex = pageIndex <= 0 ? 0 : (pageIndex - 1)
  20.  
    updateChapterIndex(index: chapter.chapterIndex)
  21.  
    self.loadPage(pageIndex: currentPageIndex)
  22.  
    if self.delegate?.reader(reader: readerProgressUpdated: curPage: totalPages: ) != nil {
  23.  
    self.delegate?.reader(reader: self, readerProgressUpdated: currentChapterIndex, curPage: currentPageIndex 1, totalPages: self.pageArrayFromCache(chapterIndex: currentChapterIndex).count)
  24.  
    }
  25.  
    }
  26.  
     
  27.  
    if isReCutPage {
  28.  
    isReCutPage = false
  29.  
    var newIndex = 1
  30.  
    for (index, item) in pages.enumerated() {
  31.  
    if prePageStartLocation >= (item.range?.location)! && prePageStartLocation <= (item.range?.location)! (item.range?.length)! {
  32.  
    newIndex = index
  33.  
    }
  34.  
    }
  35.  
    currentPageIndex = newIndex
  36.  
    self.loadPage(pageIndex: currentPageIndex)
  37.  
     
  38.  
    /// 触发预缓存
  39.  
    // self.forwardCacheIfNeed(forward: true)
  40.  
    // self.forwardCacheIfNeed(forward: false)
  41.  
    }
  42.  
     
  43.  
    if successSwitchChapter != 0 {
  44.  
    self.readChapterBy(index: successSwitchChapter, pageIndex: 1)
  45.  
    }

学新通

   小说内容,实在太多,一时不知道下手开始写这边博文,就借鉴了别人的写作思路。地址:https://segmentfault.com/a/1190000023555795

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhibcgba
系列文章
更多 icon
同类精品
更多 icon
继续加载