zoukankan      html  css  js  c++  java
  • 【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,  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

    分享链接:

    工作之余,开了一个淘宝小店,分别销售日常必备生活用品,期待您的光临!点击下图,跳转店铺首页!
  • 相关阅读:
    [LeetCode] 5. 最长回文子串
    [LeetCode] 572. 另一个树的子树
    [LeetCode] 983. 最低票价
    [LeetCode] 98. 验证二叉搜索树
    [LeetCode] 3. 无重复字符的最长子串
    [LeetCode] 21. 合并两个有序链表
    [LeetCode] 202. 快乐数
    [LeetCode] 面试题 01.07. 旋转矩阵
    [LeetCode] 面试题56
    个人网站实现支持https
  • 原文地址:https://www.cnblogs.com/xjf125/p/14308223.html
Copyright © 2011-2022 走看看