iOS: .txt 小说阅读器功能开发的 8 个老套路
本文介绍本地 .txt
小说阅读器功能开发的 8 个相关技术点。
网络 .txt 小说开发,则多了下载和缓存两步
一本书有什么,即书的数据结构
一本书有书名,有正文,有目录
手机书架上的书很多,需给书分配一个 id,去除重复
小说用户的常见操作有两种,当前阅读进度记录和书签列表
小说的主要模型 ReadModel
书的两个自然属性 ID 和目录
( 一本书有书名,这里与 ID 合并 )
书的两个用户操作属性,阅读记录和书签
( 可看出,书的正文去哪了? )
class ReadModel: NSObject,NSCoding {
/// 小说ID, 书名
let bookID:String
/// 目录, 章节列表
var chapterListModels = [ChapterBriefModel]()
/// 当前阅读记录
var recordModel:ReadRecordModel?
/// 书签列表
var markModels = [ReadMarkModel]()
}
小说的目录模型 ChapterBriefModel
class ChapterBriefModel{
/// 章节ID
var id: Int!
/// 小说ID
var bookID:String!
/// 章节名称
var name:String!
}
有了目录,要阅读,尚缺正文
小说的章节模型
包含具体的阅读章节纯文本 content,和用来渲染呈现的富文本 fullContent
含有上一章和下一章的 ID,作为一个链表,用于连续阅读
class ReadChapterModel: NSObject,NSCoding {
/// 小说ID
let bookID: String
/// 章节ID
let id: Int
/// 上一章ID
var previousChapterID: Int?
/// 下一章ID
var nextChapterID: Int?
/// 章节名称
var name:String!
/// 内容
/// 此处 content 是经过排版好且双空格开头的内容。
var content:String!
/// 完整富文本内容
var fullContent:NSAttributedString!
/// 本章有多少页
var pageCount: Int = 0
/// 分页数据
var pageModels = [ReadPageModel]()
/// 内容的排版属性
private var attributes = [NSAttributedString.Key: Any]()
}
1,基础呈现:
网上下载了一本 《三国演义》,制作一个基本的阅读界面
.txt 小说 -> 小说代码模型
小说本身自带,ID 、小说名称、章节列表和小说正文,
章节列表和小说正文有一个对应关系: 章节内容范围数组
这里把小说的 ID 和小说名称合并为一个属性
class ReadModel: NSObject,NSCoding {
/// 小说ID,使用书名作为 ID
/// 小说名称
// app 里面有很多书,有 id, 不重复
let bookID:String
/// 当前阅读记录
// 用户操作
// 初始化的时候,用户没操作,设置第一个章节为阅读记录
var recordModel:ReadRecordModel?
/// 书签列表
// 用户操作
var markModels = [ReadMarkModel]()
/// 章节列表
var chapterListModels = [ReadChapterListModel]()
/// 本地小说全文
var fullText:String!
/// 章节内容范围数组 [章节ID:[章节优先级:章节内容Range]]
var ranges:[String:[String:NSRange]]!
}
1.1 模型解析
1.1.1 数据结构
1.2 视图呈现
2,计算页码
2.1 翻页
3,目录
4,书签
5,调进度
5.1 全文的进度展示与调节
5.2 当前章节的进度展示与调节
6,翻页方式
7,更改排版方式
8,长按文本复制
关键点 3:碉堡了,我大 RxSwift 的循环引用
RxSwift 的内存管理,很少通过 weak
, 通常是手动管理,手动 dispose,
dispose , 即把相关对象置为 nil
Sink<Observer>
extension ObservableType {
/**
*/
public func withLatestFrom<Source: ObservableConvertibleType, ResultType>(_ second: Source, resultSelector: @escaping (Element, Source.Element) throws -> ResultType) -> Observable<ResultType> {
return WithLatestFrom(first: self.asObservable(), second: second.asObservable(), resultSelector: resultSelector)
}
}
做了一个简单的过滤,
去掉了拦截
一般的 RxSwift 的 Extension, 几个函数搞一下
简单的否定操作符,事件流里面的每一个事件,只需要简单处理下,
没有更多的状态管理,只需要简单的函数层次上的处理
extension ObservableType where Element == Bool {
/// 否定操作符
public func not() -> Observable<Bool> {
return self.map(!)
}
}
从 if else 开始,
事件流的逻辑控制
filter 是选取
share 是多流
事件池,当然是 RxSwift 实现的,非常好
线程锁,
异步任务,
无法保证事件完成的先后顺序
socket 多线程开发,话说那无敌的左手
不用多线程,可理解为,啥事都用好用的左右手处理
使用多线程,快多了,基本也是左右开弓
怎么怼得过那谁的三头六臂。
线程的优先级倒置,
有时候,你觉得你的 Leader 是个什么 X,
不如,换我上
线程的优先级倒置,简单理解,配置有问题
用户操作,切换线程,
线程的手动切换
解析 YYModel 源代码,学习 Runtime
提升前端素质,心中有点 B 树
平衡的树,才有用,
效率要 O ( log ( n ) ), 不要 O ( n )
O ( n ) 是线性查找,也就是链表
B 树,是一颗好树
好的树,便于查找,search 操作的复杂度是 O(log (n)
好的树,增加和删除节点后,可以维持自己的结构,这个意思是,一直都便于查找。
不可维持自己的结构的树,只能爽一次
维持自己的结构的树,search 操作的复杂度保持在 O(log (n),品质始终如一
B 树,一个节点,可以包含多个 KV,有效的降低了树的高度。
B 树,有一个 degree ( n ), 也就是一个节点最多可以有 n 个内容,n + 1 个 children.
节点包含的内容,与节点孩子的关系:
n 个可比较的内容,可以分割出 n + 1 个孩子
n 个可比较的内容,可以分割出 n + 1 段范围,每一颗子树,存在于指定的范围
就像黄瓜,切四刀,可以吃五口 ( 一般的刀法下 )
B 树,不是二叉搜索树,譬如: AVL , 红黑树
B 树的孩子挺多的
B 树,是一颗平衡的树,
AVL 通过记录节点的高度,
红黑树,通过
B 树通过持有多节点
B 树的孩子挺多的
这样有效降低了层级,也就是树的高度,降下来了
从下往上生长
添加,删除
自保持,自平衡,自我维持住,
便于查找,
查找效率高
数据库,什么系统的刚需
红黑树,
要区分,就要做标记
划分 MVC , 是为了 代码 分层、 分区,隔离 不同功能的代码, 数据 流 上。
在代码 数据流向 模块间 竖起 明显的隔离线。
感觉 , 有三个 Model .
Service , 一统 外部数据。网络请求, 本地 数据库。
DataSource, 遵守 *UITableViewDataSource * Protocol , 有 ViewModel 的 感觉。
一统 内部数据。 就是 显示数据与操作数据。 给用户看的, 和 用的。Model ,数据解析 序列化。 与后台 约定的数据结构,申明的 一大堆属性,
与简单的 业务数据 处理, mapper 出,提供 我方需要的 结构化的 数据。
前端 的 Controller
是 用户
(
只有 View 和 Model,
就是 视频 、 动画
)
MVCS
Store, Service.
配合 Aggregate Data Source.
在 dataSource 中, 处理 cell 数据。
将 数据 判断结果 甩出去。
DataSource
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyProductsCell *cell = [tableView dequeueReusableCellWithIdentifier: self.cellIdentifier forIndexPath: indexPath];
cell.myProduct = self.myProductItems[indexPath.row];
BOOL isCellContained = [self.tempSelectedArray containsObject: self.myProductItems[indexPath.row]];
if (self.myProductsCtrlDataSourceBlock) {
self.myProductsCtrlDataSourceBlock(cell, self.myProductItems[indexPath.row], isCellContained, indexPath);
}
return cell;
}
VC:
- (void)networkMyProductsCtrlStoreSuccess:(id)successData failure:(NSString *)failureStr;{
if (successData) {
NSArray *dataArray = (NSArray *)successData;
if (dataArray.count == 0) {
self.networkResult = EmptyData;
}
NSMutableArray *myProductsTempArray = [NSMutableArray array];
for (NSDictionary *dict in dataArray) {
MyProduct *myProducts = [MyProduct yy_modelWithDictionary:dict];
[myProductsTempArray addObject:myProducts];
}
self.myProductsCtrlDataSource = [[MyProductsCtrlDataSource alloc] initWithItems: myProductsTempArray cellIdentifier: kMyProductsCell configureCellBlock:^(MyProductsCell *cell, MyProduct *MyProduct, BOOL isCellContained, NSIndexPath *indexPath) {
if (isCellContained && self.myProductsTableView.editing) {
[self.myProductsTableView selectRowAtIndexPath: indexPath animated:YES scrollPosition:(UITableViewScrollPositionNone)];
}
}];
相同点,内部有 各种 分门别类的数据源, 经过 外部的简单参数,
相当于 各种材料 都在 手上,非常集中 ,非常爽滴 进行 复杂运算,数据加工,
返回给外部。
很明显, 这就是一层
Model Layer.
一层,就是 一统。
高度封装,就是高度定制化。
Store
外部数据 统一处理,
网络 与 持久化数据库
包括, 各种网络请求 增删改。
好像并没有 直接的 关联。
其实 就是 直接在里面 用Net API match 几下,改下实例 存储的 临时变量, 返回给外部 结果。
Aggregate TableView DataSource
内部数据,统一处理
getter
暴露出 readonly 的 数组,供外部查询。
setter
暴露出数据操作方法。
高度封装。
相当于API 调用。
外部传几个 很简单的 参数,
内部各种数据运算出结果,返回外部。
如果是,MVC,
VC 中
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyProductsCell *cell = [tableView dequeueReusableCellWithIdentifier: self.cellIdentifier forIndexPath: indexPath];
cell.myProduct = self.myProductItems[indexPath.row];
// cell.delegate = self;
if ([self.tempSelectedArray containsObject:cell.myProduct] && self.myProductsTableView.editing) {
[tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:(UITableViewScrollPositionNone)];
}
return cell;
}
不同的拆分,不同的写法, 实质上是不同的 逻辑思想,不仅仅是 对应 逻辑的翻译。就是 相同业务逻辑的翻译,使用胶水代码/语法糖
MVVM. 响应式。data binding.
否则 在 JS 中,又要改 视图,又要 改数据源。
传值,每次都要改变的,
用函数行为参数,
用 argument.
状态 需要保存一会的,
用 属性。
本地搜索,
ML,
文字匹配,
文字转拼音 匹配
Mac three
local player
Realm , AudioKit