公司項目結束了,公司估計也快黃了,年末事少,也給了我很多時間來維護博客。segmentfault
公司的項目是一個相似於簡書的創做平臺,涵蓋寫做、小說、插畫內容。緩存
本期主要先下小說閱讀部分,UI樣式仿照的是微信讀書樣式,因以前也寫太小說閱讀器,可是代碼並無解耦,此次不折不扣作一次大改動。微信
小說用戶的常見操做:當前閱讀進入記錄和書籤列表,因公司項目的結構問題,目前新項目並無作項目進度記錄和書籤保存功能,之後有優化時候,再補充相關內容。先看下小說的結構。數據結構
小說的主要模型ReadModelapp
小說章節模型curl
class JFChapterModel: NSObject { var title: String? var path: String? var chapterIndex: Int = 1 }
小說頁面Model,一個頁面,就是一個Modelasync
class JFPageModel: NSObject { var attributedString: NSAttributedString? var range: NSRange? var pageIndex: Int = 1 }
一本書的數據結構確立後,進入功能開發ide
一、把資源路徑轉化爲正文,解析出全部的章節目錄,把正文做爲一個字符串,正則拆分出全部的章節,映射爲ChapterModelpost
首先正則獲取章節目錄優化
func doTitleMatchWith(content: String) -> [NSTextCheckingResult] { let pattern = "第[ ]*[0-9一二三四五六七八九十百千]*[ ]*[章回].*" let regExp = try! NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options.caseInsensitive) let results = regExp.matches(in: content, options: .reportCompletion, range: NSMakeRange(0, content.count)) return results }
let content = path var models = Array<JFChapterModel>() var titles = Array<String>() DispatchQueue.global().async { let document = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).first let fileName = name let bookPath = document! + "/\(String(fileName))" if FileManager.default.fileExists(atPath: bookPath) == false { try? FileManager.default.createDirectory(atPath: bookPath, withIntermediateDirectories: true, attributes: nil) } let results = self.doTitleMatchWith(content: content) if results.count == 0 { let model = JFChapterModel() model.chapterIndex = 1 model.path = path completeHandler([], [model]) }else { var endIndex = content.startIndex for (index, result) in results.enumerated() { let startIndex = content.index(content.startIndex, offsetBy: result.range.location) endIndex = content.index(startIndex, offsetBy: result.range.length) let currentTitle = String(content[startIndex...endIndex]) titles.append(currentTitle) let chapterPath = bookPath + "/chapter" + String(index + 1) + ".txt" let model = JFChapterModel() model.chapterIndex = index + 1 model.title = currentTitle model.path = chapterPath models.append(model) if FileManager.default.fileExists(atPath: chapterPath) { continue } var endLoaction = 0 if index == results.count - 1 { endLoaction = content.count - 1 }else { endLoaction = results[index + 1].range.location - 1 } let startLocation = content.index(content.startIndex, offsetBy: result.range.location) let subString = String(content[startLocation...content.index(content.startIndex, offsetBy: endLoaction)]) try! subString.write(toFile: chapterPath, atomically: true, encoding: String.Encoding.utf8) } DispatchQueue.main.async { completeHandler(titles, models) } } }
拿到閱讀模型後,展現出來,就能夠看書了。
翻頁模式,有仿真、平移和滾動
這裏以仿真爲例子:
仿真的效果,使用 UIPageViewController
先添加 UIPageViewController 的視圖,到閱讀容器視圖 contentView 上面
private func loadPageViewController() -> Void { self.clearReaderViewIfNeed() let transtionStyle: UIPageViewController.TransitionStyle = (self.config.scrollType == .curl) ? .pageCurl : .scroll self.pageVC = JFContainerPageViewController(transitionStyle: transtionStyle, navigationOrientation: .horizontal, options: nil) self.pageVC?.dataSource = self self.pageVC?.delegate = self self.pageVC?.view.backgroundColor = UIColor.clear // 翻頁背部帶文字效果 self.pageVC?.isDoubleSided = (self.config.scrollType == .curl) ? true : false self.addChild(self.pageVC!) self.view.addSubview((self.pageVC?.view)!) self.pageVC?.didMove(toParent: self) }
如下是獲取下一頁的代碼,
獲取上一頁的,相似
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { print("向後翻頁 -------1") struct LastPage { static var arrived = false } let nextIndex: Int let pageArray = self.pageArrayFromCache(chapterIndex: currentChapterIndex) if viewController is JFPageViewController { let page = viewController as! DUAPageViewController nextIndex = page.index + 1 if nextIndex == pageArray.count { LastPage.arrived = true } let backPage = JFBackViewController() backPage.grabViewController(viewController: page) return backPage } if LastPage.arrived { LastPage.arrived = false if currentChapterIndex + 1 > totalChapterModels.count { return nil } pageVC?.willStepIntoNextChapter = true self.requestChapterWith(index: currentChapterIndex + 1) let nextPage = self.getPageVCWith(pageIndex: 0, chapterIndex: currentChapterIndex + 1) /// 須要的頁面並無準備好,此時出現頁面飢餓 if nextPage == nil { self.postReaderStateNotification(state: .busy) pageHunger = true } return nextPage } let back = viewController as! JFBackViewController return self.getPageVCWith(pageIndex: back.index + 1, chapterIndex: back.chapterBelong) }
一個章節有幾頁,是怎麼計算出來的?
先拿着一個章節的富文本,和顯示區域,計算出書頁的範圍
一般顯示區域,是放不滿一章的。
顯示區域先放一頁,獲得這一頁的開始範圍和長度,對應一個 ReadPageModel
顯示區域再放下一頁 ...
let layouter = JFCoreTextLayouter.init(attributedString: attrString) let rect = CGRect(x: config.contentFrame.origin.x, y: config.contentFrame.origin.y, width: config.contentFrame.size.width, height: config.contentFrame.size.height - 5) var frame = layouter?.layoutFrame(with: rect, range: NSRange(location: 0, length: attrString.length)) var pageVisibleRange = frame?.visibleStringRange() var rangeOffset = pageVisibleRange!.location + pageVisibleRange!.length
拿上一步計算出來的範圍,建立該章節每一頁的模型 ReadPageModel
while rangeOffset <= attrString.length && rangeOffset != 0 { let pageModel = DUAPageModel.init() pageModel.attributedString = attrString.attributedSubstring(from: pageVisibleRange!) pageModel.range = pageVisibleRange pageModel.pageIndex = count - 1 frame = layouter?.layoutFrame(with: rect, range: NSRange(location: rangeOffset, length: attrString.length - rangeOffset)) pageVisibleRange = frame?.visibleStringRange() if pageVisibleRange == nil { rangeOffset = 0 }else { rangeOffset = pageVisibleRange!.location + pageVisibleRange!.length } let completed = (rangeOffset <= attrString.length && rangeOffset != 0) ? false : true completeHandler(count, pageModel, completed) count += 1 }
獲取下一頁的代碼
翻一頁,就是當前的 RecordModel , 翻到下一頁,
交給閱讀控制器去呈現, ReadViewController 的子類 ReadLongPressViewController
func setViewController(viewController: UIViewController, direction: translationControllerNavigationDirection, animated: Bool, completionHandler: ((Bool) -> Void)?) -> Void { if animated == false { for controller in self.children { self.removeController(controller: controller) } self.addController(controller: viewController) if completionHandler != nil { completionHandler!(true) } }else { let oldController = self.children.first self.addController(controller: viewController) var newVCEndTransform: CGAffineTransform var oldVCEndTransform: CGAffineTransform viewController.view.transform = .identity if direction == .left { viewController.view.transform = CGAffineTransform(translationX: screenWidth, y: 0) newVCEndTransform = .identity oldController?.view.transform = .identity oldVCEndTransform = CGAffineTransform(translationX: -screenWidth, y: 0) }else { viewController.view.transform = CGAffineTransform(translationX: -screenWidth, y: 0) newVCEndTransform = .identity oldController?.view.transform = .identity oldVCEndTransform = CGAffineTransform(translationX: screenWidth, y: 0) } UIView.animate(withDuration: animationDuration, animations: { oldController?.view.transform = oldVCEndTransform viewController.view.transform = newVCEndTransform }, completion: { (complete) in if complete { self.removeController(controller: oldController!) } if completionHandler != nil { completionHandler!(complete) } }) } }
//若是到了最後一章、最後一頁時,就翻不動了
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 <= 0 ? 0 : (pageIndex - 1) updateChapterIndex(index: chapter.chapterIndex) self.loadPage(pageIndex: currentPageIndex) if self.delegate?.reader(reader: readerProgressUpdated: curPage: totalPages: ) != nil { self.delegate?.reader(reader: self, readerProgressUpdated: currentChapterIndex, curPage: currentPageIndex + 1, totalPages: self.pageArrayFromCache(chapterIndex: currentChapterIndex).count) } } if isReCutPage { isReCutPage = false var newIndex = 1 for (index, item) in pages.enumerated() { if prePageStartLocation >= (item.range?.location)! && prePageStartLocation <= (item.range?.location)! + (item.range?.length)! { newIndex = index } } currentPageIndex = newIndex self.loadPage(pageIndex: currentPageIndex) /// 觸發預緩存 // self.forwardCacheIfNeed(forward: true) // self.forwardCacheIfNeed(forward: false) } if successSwitchChapter != 0 { self.readChapterBy(index: successSwitchChapter, pageIndex: 1) }
小說內容,實在太多,一時不知道下手開始寫這邊博文,就借鑑了別人的寫做思路。地址:https://segmentfault.com/a/1190000023555795