找回密码
 立即注册
首页 资源区 代码 【Swift】拆分小说阅读器功能,分享内部实现 ...

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

倡粤 2025-6-4 20:10:16
  公司项目结束了,公司估计也快黄了,年底事少,也给了我不少时间来维护博客。
  公司的项目是一个类似于简书的创作平台,涵盖写作、小说、插画内容。
  本期主要先下小说阅读部分,UI样式仿照的是微信读书样式,因之前也写过小说阅读器,但是代码并没有解耦,这次彻彻底底做一次大改动。
   小说用户的常见操作:当前阅读进入记录和书签列表,因公司项目的结构问题,目前新项目并没有做项目进度记录和书签保存功能,以后有优化时候,再补充相关内容。先看下小说的结构。
1.png

 
  小说的主要模型ReadModel
  小说章节模型
  1. class JFChapterModel: NSObject {
  2.     var title: String?
  3.     var path: String?
  4.     var chapterIndex: Int = 1
  5. }
复制代码
  小说页面Model,一个页面,就是一个Model
  1. class JFPageModel: NSObject {
  2.     var attributedString: NSAttributedString?
  3.     var range: NSRange?
  4.     var pageIndex: Int = 1
  5. }
复制代码
  一本书的数据结构确立后,进入功能开发
  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.                 for (index, result) in results.enumerated() {
  21.                     let startIndex = content.index(content.startIndex, offsetBy: result.range.location)
  22.                     endIndex = content.index(startIndex, offsetBy: result.range.length)
  23.                     let currentTitle = String(content[startIndex...endIndex])
  24.                     titles.append(currentTitle)
  25.                     let chapterPath = bookPath + "/chapter" + String(index + 1) + ".txt"
  26.                     let model = JFChapterModel()
  27.                     model.chapterIndex = index + 1
  28.                     model.title = currentTitle
  29.                     model.path = chapterPath
  30.                     models.append(model)
  31.                     if FileManager.default.fileExists(atPath: chapterPath) {
  32.                         continue
  33.                     }
  34.                     var endLoaction = 0
  35.                     if index == results.count - 1 {
  36.                         endLoaction = content.count - 1
  37.                     }else {
  38.                         endLoaction = results[index + 1].range.location - 1
  39.                     }
  40.                     let startLocation = content.index(content.startIndex, offsetBy: result.range.location)
  41.                     let subString = String(content[startLocation...content.index(content.startIndex, offsetBy: endLoaction)])
  42.                     try! subString.write(toFile: chapterPath, atomically: true, encoding: String.Encoding.utf8)
  43.                 }
  44.                 DispatchQueue.main.async {
  45.                     completeHandler(titles, models)
  46.                 }
  47.             }
  48.         }
复制代码
  拿到阅读模型后,展示出来,就可以看书了。
  2、翻页模式处理

  翻页模式,有仿真、平移和滚动
  这里以仿真为例子:
  仿真的效果,使用 UIPageViewController
  先添加 UIPageViewController 的视图,到阅读容器视图 contentView 上面
  1. private func loadPageViewController() -> Void {
  2.         self.clearReaderViewIfNeed()
  3.         let transtionStyle: UIPageViewController.TransitionStyle = (self.config.scrollType == .curl) ? .pageCurl : .scroll
  4.         self.pageVC = JFContainerPageViewController(transitionStyle: transtionStyle, navigationOrientation: .horizontal, options: nil)
  5.         self.pageVC?.dataSource = self
  6.         self.pageVC?.delegate = self
  7.         self.pageVC?.view.backgroundColor = UIColor.clear
  8.         
  9.         // 翻页背部带文字效果
  10.         self.pageVC?.isDoubleSided = (self.config.scrollType == .curl) ? true : false
  11.         
  12.         self.addChild(self.pageVC!)
  13.         self.view.addSubview((self.pageVC?.view)!)
  14.         self.pageVC?.didMove(toParent: self)
  15.     }
复制代码

  • 提供分页控制器的内容,即阅读内容
  以下是获取下一页的代码,
  获取上一页的,类似
  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.         }
复制代码
  //如果到了最后一章、最后一页时,就翻不动了
[code]self.postReaderStateNotification(state: .ready)        if pageHunger {            pageHunger = false            if pageVC != nil {                self.loadPage(pageIndex: currentPageIndex)            }            if tableView != nil {                if currentPageIndex == 0 && tableView?.scrollDirection == .up {                    self.requestLastChapterForTableView()                }                if currentPageIndex == self.pageArrayFromCache(chapterIndex: currentChapterIndex).count - 1 && tableView?.scrollDirection == .down {                    self.requestNextChapterForTableView()                }            }        }                if firstIntoReader {            firstIntoReader = false            currentPageIndex = pageIndex = (item.range?.location)! && prePageStartLocation
您需要登录后才可以回帖 登录 | 立即注册