【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) -> {
let pattern = "第[ ]**[ ]*[章回].*"
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([], )
}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)
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.range.location - 1
}
let startLocation = content.index(content.startIndex, offsetBy: result.range.location)
let subString = String(content)
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
} //如果到了最后一章、最后一页时,就翻不动了
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
页:
[1]