【Swift】拆分小說閱讀器功能,分享內部實現

  公司項目結束了,公司估計也快黃了,年底事少,也給了我不少時間來維護博客。

  公司的項目是一個類似於簡書的創作平臺,涵蓋寫作、小說、插畫內容。

  本期主要先下小說閱讀部分,UI樣式仿照的是微信讀書樣式,因之前也寫過小說閱讀器,但是代碼並沒有解耦,這次徹徹底底做一次大改動。

   小說用戶的常見操作:當前閱讀進入記錄和書籤列表,因公司項目的結構問題,目前新項目並沒有做項目進度記錄和書籤保存功能,以後有優化時候,再補充相關內容。先看下小說的結構。

 

  小說的主要模型ReadModel

  小說章節模型

class JFChapterModel: NSObject {

    var title: String?
    var path: String?
    var chapterIndex: Int = 1
}

  小說頁面Model,一個頁面,就是一個Model

class JFPageModel: NSObject {

    var attributedString: NSAttributedString?
    var range: NSRange?
    var pageIndex: Int = 1

}

  一本書的數據結構確立後,進入功能開發

  1、模型解析

  1、把資源路徑轉化爲正文,解析出所有的章節目錄,把正文作爲一個字符串,正則拆分出所有的章節,映射爲ChapterModel

  首先正則獲取章節目錄

    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)
                }
            }
        }

  拿到閱讀模型後,展示出來,就可以看書了。

  2、翻頁模式處理

  翻頁模式,有仿真、平移和滾動

  這裏以仿真爲例子:

  仿真的效果,使用 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)
    }

  3、計算頁碼

  一個章節有幾頁,是怎麼計算出來的?

  先拿着一個章節的富文本,和顯示區域,計算出書頁的範圍

  通常顯示區域,是放不滿一章的。

  顯示區域先放一頁,得到這一頁的開始範圍和長度,對應一個 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
        }

  4、翻頁

  獲取下一頁的代碼

  翻一頁,就是當前的 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章