经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 移动开发 » Swift » 查看文章
【Swift】拆分小说阅读器功能,分享内部实现
来源:cnblogs  作者:Mr丶Landen  时间:2021/1/25 11:14:56  对本文有异议

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

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

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

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

 

  小说的主要模型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. let results = self.doTitleMatchWith(content: content)
  12. if results.count == 0 {
  13. let model = JFChapterModel()
  14. model.chapterIndex = 1
  15. model.path = path
  16. completeHandler([], [model])
  17. }else {
  18. var endIndex = content.startIndex
  19. for (index, result) in results.enumerated() {
  20. let startIndex = content.index(content.startIndex, offsetBy: result.range.location)
  21. endIndex = content.index(startIndex, offsetBy: result.range.length)
  22. let currentTitle = String(content[startIndex...endIndex])
  23. titles.append(currentTitle)
  24. let chapterPath = bookPath + "/chapter" + String(index + 1) + ".txt"
  25. let model = JFChapterModel()
  26. model.chapterIndex = index + 1
  27. model.title = currentTitle
  28. model.path = chapterPath
  29. models.append(model)
  30. if FileManager.default.fileExists(atPath: chapterPath) {
  31. continue
  32. }
  33. var endLoaction = 0
  34. if index == results.count - 1 {
  35. endLoaction = content.count - 1
  36. }else {
  37. endLoaction = results[index + 1].range.location - 1
  38. }
  39. let startLocation = content.index(content.startIndex, offsetBy: result.range.location)
  40. let subString = String(content[startLocation...content.index(content.startIndex, offsetBy: endLoaction)])
  41. try! subString.write(toFile: chapterPath, atomically: true, encoding: String.Encoding.utf8)
  42. }
  43. DispatchQueue.main.async {
  44. completeHandler(titles, models)
  45. }
  46. }
  47. }

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

  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. self.pageVC?.isDoubleSided = (self.config.scrollType == .curl) ? true : false
  10. self.addChild(self.pageVC!)
  11. self.view.addSubview((self.pageVC?.view)!)
  12. self.pageVC?.didMove(toParent: self)
  13. }
  • 提供分页控制器的内容,即阅读内容

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

  获取上一页的,类似

  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. var pageVisibleRange = frame?.visibleStringRange()
  5. 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. frame = layouter?.layoutFrame(with: rect, range: NSRange(location: rangeOffset, length: attrString.length - rangeOffset))
  7. pageVisibleRange = frame?.visibleStringRange()
  8. if pageVisibleRange == nil {
  9. rangeOffset = 0
  10. }else {
  11. rangeOffset = pageVisibleRange!.location + pageVisibleRange!.length
  12. }
  13. let completed = (rangeOffset <= attrString.length && rangeOffset != 0) ? false : true
  14. completeHandler(count, pageModel, completed)
  15. count += 1
  16. }

  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. var newVCEndTransform: CGAffineTransform
  14. var oldVCEndTransform: CGAffineTransform
  15. viewController.view.transform = .identity
  16. if direction == .left {
  17. viewController.view.transform = CGAffineTransform(translationX: screenWidth, y: 0)
  18. newVCEndTransform = .identity
  19. oldController?.view.transform = .identity
  20. oldVCEndTransform = CGAffineTransform(translationX: -screenWidth, y: 0)
  21. }else {
  22. viewController.view.transform = CGAffineTransform(translationX: -screenWidth, y: 0)
  23. newVCEndTransform = .identity
  24. oldController?.view.transform = .identity
  25. oldVCEndTransform = CGAffineTransform(translationX: screenWidth, y: 0)
  26. }
  27. UIView.animate(withDuration: animationDuration, animations: {
  28. oldController?.view.transform = oldVCEndTransform
  29. viewController.view.transform = newVCEndTransform
  30. }, completion: { (complete) in
  31. if complete {
  32. self.removeController(controller: oldController!)
  33. }
  34. if completionHandler != nil {
  35. completionHandler!(complete)
  36. }
  37. })
  38. }
  39. }

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

  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. if firstIntoReader {
  17. firstIntoReader = false
  18. currentPageIndex = pageIndex <= 0 ? 0 : (pageIndex - 1)
  19. updateChapterIndex(index: chapter.chapterIndex)
  20. self.loadPage(pageIndex: currentPageIndex)
  21. if self.delegate?.reader(reader: readerProgressUpdated: curPage: totalPages: ) != nil {
  22. self.delegate?.reader(reader: self, readerProgressUpdated: currentChapterIndex, curPage: currentPageIndex + 1, totalPages: self.pageArrayFromCache(chapterIndex: currentChapterIndex).count)
  23. }
  24. }
  25. if isReCutPage {
  26. isReCutPage = false
  27. var newIndex = 1
  28. for (index, item) in pages.enumerated() {
  29. if prePageStartLocation >= (item.range?.location)! && prePageStartLocation <= (item.range?.location)! + (item.range?.length)! {
  30. newIndex = index
  31. }
  32. }
  33. currentPageIndex = newIndex
  34. self.loadPage(pageIndex: currentPageIndex)
  35. /// 触发预缓存
  36. // self.forwardCacheIfNeed(forward: true)
  37. // self.forwardCacheIfNeed(forward: false)
  38. }
  39. if successSwitchChapter != 0 {
  40. self.readChapterBy(index: successSwitchChapter, pageIndex: 1)
  41. }

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

原文链接:http://www.cnblogs.com/xjf125/p/14308223.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号