iOS: .txt 小說(shuō)閱讀器功能開發(fā)的 8 個(gè)老套路
本文介紹本地 .txt 小說(shuō)閱讀器功能開發(fā)的 8 個(gè)相關(guān)技術(shù)點(diǎn)。
網(wǎng)絡(luò) .txt 小說(shuō)開發(fā),則多了下載和緩存兩步
一本書有什么,即書的數(shù)據(jù)結(jié)構(gòu)
一本書有書名,有正文,有目錄
手機(jī)書架上的書很多,需給書分配一個(gè) id,去除重復(fù)
小說(shuō)用戶的常見操作有兩種,當(dāng)前閱讀進(jìn)度記錄和書簽列表
小說(shuō)的主要模型 ReadModel
書的兩個(gè)自然屬性 ID 和目錄
( 一本書有書名,這里與 ID 合并 )
書的兩個(gè)用戶操作屬性,閱讀記錄和書簽
( 可看出,書的正文去哪了? )
class ReadModel: NSObject,NSCoding {
/// 小說(shuō)ID, 書名
let bookID:String
/// 目錄, 章節(jié)列表
var chapterListModels = [ChapterBriefModel]()
/// 當(dāng)前閱讀記錄
var recordModel:ReadRecordModel?
/// 書簽列表
var markModels = [ReadMarkModel]()
}
小說(shuō)的目錄模型 ChapterBriefModel
class ChapterBriefModel{
/// 章節(jié)ID
var id: Int!
/// 小說(shuō)ID
var bookID:String!
/// 章節(jié)名稱
var name:String!
}
有了目錄,要閱讀,尚缺正文
小說(shuō)的章節(jié)模型
包含具體的閱讀章節(jié)純文本 content,和用來(lái)渲染呈現(xiàn)的富文本 fullContent
含有上一章和下一章的 ID,作為一個(gè)鏈表,用于連續(xù)閱讀
class ReadChapterModel: NSObject,NSCoding {
/// 小說(shuō)ID
let bookID: String
/// 章節(jié)ID
let id: Int
/// 上一章ID
var previousChapterID: Int?
/// 下一章ID
var nextChapterID: Int?
/// 章節(jié)名稱
var name:String!
/// 內(nèi)容
/// 此處 content 是經(jīng)過排版好且雙空格開頭的內(nèi)容。
var content:String!
/// 完整富文本內(nèi)容
var fullContent:NSAttributedString!
/// 本章有多少頁(yè)
var pageCount: Int = 0
/// 分頁(yè)數(shù)據(jù)
var pageModels = [ReadPageModel]()
/// 內(nèi)容的排版屬性
private var attributes = [NSAttributedString.Key: Any]()
}
1,基礎(chǔ)呈現(xiàn):
網(wǎng)上下載了一本 《三國(guó)演義》,制作一個(gè)基本的閱讀界面
.txt 小說(shuō) -> 小說(shuō)代碼模型
小說(shuō)本身自帶,ID 、小說(shuō)名稱、章節(jié)列表和小說(shuō)正文,
章節(jié)列表和小說(shuō)正文有一個(gè)對(duì)應(yīng)關(guān)系: 章節(jié)內(nèi)容范圍數(shù)組
這里把小說(shuō)的 ID 和小說(shuō)名稱合并為一個(gè)屬性
class ReadModel: NSObject,NSCoding {
/// 小說(shuō)ID,使用書名作為 ID
/// 小說(shuō)名稱
// app 里面有很多書,有 id, 不重復(fù)
let bookID:String
/// 當(dāng)前閱讀記錄
// 用戶操作
// 初始化的時(shí)候,用戶沒操作,設(shè)置第一個(gè)章節(jié)為閱讀記錄
var recordModel:ReadRecordModel?
/// 書簽列表
// 用戶操作
var markModels = [ReadMarkModel]()
/// 章節(jié)列表
var chapterListModels = [ReadChapterListModel]()
/// 本地小說(shuō)全文
var fullText:String!
/// 章節(jié)內(nèi)容范圍數(shù)組 [章節(jié)ID:[章節(jié)優(yōu)先級(jí):章節(jié)內(nèi)容Range]]
var ranges:[String:[String:NSRange]]!
}
1.1 模型解析
1.1.1 數(shù)據(jù)結(jié)構(gòu)
1.2 視圖呈現(xiàn)
2,計(jì)算頁(yè)碼
2.1 翻頁(yè)
3,目錄
4,書簽
5,調(diào)進(jìn)度
5.1 全文的進(jìn)度展示與調(diào)節(jié)
5.2 當(dāng)前章節(jié)的進(jìn)度展示與調(diào)節(jié)
6,翻頁(yè)方式
7,更改排版方式
8,長(zhǎng)按文本復(fù)制
關(guān)鍵點(diǎn) 3:碉堡了,我大 RxSwift 的循環(huán)引用
RxSwift 的內(nèi)存管理,很少通過 weak, 通常是手動(dòng)管理,手動(dòng) dispose,
dispose , 即把相關(guān)對(duì)象置為 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)
}
}
做了一個(gè)簡(jiǎn)單的過濾,
去掉了攔截
一般的 RxSwift 的 Extension, 幾個(gè)函數(shù)搞一下
簡(jiǎn)單的否定操作符,事件流里面的每一個(gè)事件,只需要簡(jiǎn)單處理下,
沒有更多的狀態(tài)管理,只需要簡(jiǎn)單的函數(shù)層次上的處理
extension ObservableType where Element == Bool {
/// 否定操作符
public func not() -> Observable<Bool> {
return self.map(!)
}
}
從 if else 開始,
事件流的邏輯控制
filter 是選取
share 是多流
事件池,當(dāng)然是 RxSwift 實(shí)現(xiàn)的,非常好
線程鎖,
異步任務(wù),
無(wú)法保證事件完成的先后順序
socket 多線程開發(fā),話說(shuō)那無(wú)敵的左手
不用多線程,可理解為,啥事都用好用的左右手處理
使用多線程,快多了,基本也是左右開弓
怎么懟得過那誰(shuí)的三頭六臂。
?
線程的優(yōu)先級(jí)倒置,
有時(shí)候,你覺得你的 Leader 是個(gè)什么 X,
不如,換我上
線程的優(yōu)先級(jí)倒置,簡(jiǎn)單理解,配置有問題
?
用戶操作,切換線程,
線程的手動(dòng)切換
解析 YYModel 源代碼,學(xué)習(xí) Runtime
提升前端素質(zhì),心中有點(diǎn) B 樹
平衡的樹,才有用,
效率要 O ( log ( n ) ), 不要 O ( n )
O ( n ) 是線性查找,也就是鏈表
?
?
B 樹,是一顆好樹
好的樹,便于查找,search 操作的復(fù)雜度是 O(log (n)
好的樹,增加和刪除節(jié)點(diǎn)后,可以維持自己的結(jié)構(gòu),這個(gè)意思是,一直都便于查找。
不可維持自己的結(jié)構(gòu)的樹,只能爽一次
維持自己的結(jié)構(gòu)的樹,search 操作的復(fù)雜度保持在 O(log (n),品質(zhì)始終如一
B 樹,一個(gè)節(jié)點(diǎn),可以包含多個(gè) KV,有效的降低了樹的高度。
B 樹,有一個(gè) degree ( n ), 也就是一個(gè)節(jié)點(diǎn)最多可以有 n 個(gè)內(nèi)容,n + 1 個(gè) children.
節(jié)點(diǎn)包含的內(nèi)容,與節(jié)點(diǎn)孩子的關(guān)系:
n 個(gè)可比較的內(nèi)容,可以分割出 n + 1 個(gè)孩子
n 個(gè)可比較的內(nèi)容,可以分割出 n + 1 段范圍,每一顆子樹,存在于指定的范圍
就像黃瓜,切四刀,可以吃五口 ( 一般的刀法下 )
?
?
B 樹,不是二叉搜索樹,譬如: AVL , 紅黑樹
B 樹的孩子挺多的
B 樹,是一顆平衡的樹,
AVL 通過記錄節(jié)點(diǎn)的高度,
紅黑樹,通過
B 樹通過持有多節(jié)點(diǎn)
B 樹的孩子挺多的
這樣有效降低了層級(jí),也就是樹的高度,降下來(lái)了
?
從下往上生長(zhǎng)
?
添加,刪除
自保持,自平衡,自我維持住,
便于查找,
查找效率高
數(shù)據(jù)庫(kù),什么系統(tǒng)的剛需
?
?
紅黑樹,
要區(qū)分,就要做標(biāo)記
?
?
劃分 MVC , 是為了 代碼 分層、 分區(qū),隔離 不同功能的代碼, 數(shù)據(jù) 流 上。
在代碼 數(shù)據(jù)流向 模塊間 豎起 明顯的隔離線。
?
感覺 , 有三個(gè) Model .
Service , 一統(tǒng) 外部數(shù)據(jù)。網(wǎng)絡(luò)請(qǐng)求, 本地 數(shù)據(jù)庫(kù)。
DataSource, 遵守 *UITableViewDataSource * Protocol , 有 ViewModel 的 感覺。
一統(tǒng) 內(nèi)部數(shù)據(jù)。 就是 顯示數(shù)據(jù)與操作數(shù)據(jù)。 給用戶看的, 和 用的。Model ,數(shù)據(jù)解析 序列化。 與后臺(tái) 約定的數(shù)據(jù)結(jié)構(gòu),申明的 一大堆屬性,
與簡(jiǎn)單的 業(yè)務(wù)數(shù)據(jù) 處理, mapper 出,提供 我方需要的 結(jié)構(gòu)化的 數(shù)據(jù)。
??
前端 的 Controller
是 用戶
(
只有 View 和 Model,
就是 視頻 、 動(dòng)畫
)
??
MVCS
Store, Service.
配合 Aggregate Data Source.
在 dataSource 中, 處理 cell 數(shù)據(jù)。
將 數(shù)據(jù) 判斷結(jié)果 甩出去。
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)];
}
}];
?
相同點(diǎn),內(nèi)部有 各種 分門別類的數(shù)據(jù)源, 經(jīng)過 外部的簡(jiǎn)單參數(shù),
相當(dāng)于 各種材料 都在 手上,非常集中 ,非常爽滴 進(jìn)行 復(fù)雜運(yùn)算,數(shù)據(jù)加工,
返回給外部。
很明顯, 這就是一層
Model Layer.
一層,就是 一統(tǒng)。
高度封裝,就是高度定制化。
Store
外部數(shù)據(jù) 統(tǒng)一處理,
網(wǎng)絡(luò) 與 持久化數(shù)據(jù)庫(kù)
包括, 各種網(wǎng)絡(luò)請(qǐng)求 增刪改。
好像并沒有 直接的 關(guān)聯(lián)。
其實(shí) 就是 直接在里面 用Net API match 幾下,改下實(shí)例 存儲(chǔ)的 臨時(shí)變量, 返回給外部 結(jié)果。
?
Aggregate TableView DataSource
內(nèi)部數(shù)據(jù),統(tǒng)一處理
getter
暴露出 readonly 的 數(shù)組,供外部查詢。
setter
暴露出數(shù)據(jù)操作方法。
高度封裝。
相當(dāng)于API 調(diào)用。
外部傳幾個(gè) 很簡(jiǎn)單的 參數(shù),
內(nèi)部各種數(shù)據(jù)運(yùn)算出結(jié)果,返回外部。
?
如果是,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;
}
不同的拆分,不同的寫法, 實(shí)質(zhì)上是不同的 邏輯思想,不僅僅是 對(duì)應(yīng) 邏輯的翻譯。就是 相同業(yè)務(wù)邏輯的翻譯,使用膠水代碼/語(yǔ)法糖
MVVM. 響應(yīng)式。data binding.
否則 在 JS 中,又要改 視圖,又要 改數(shù)據(jù)源。
?
傳值,每次都要改變的,
用函數(shù)行為參數(shù),
用 argument.
狀態(tài) 需要保存一會(huì)的,
用 屬性。
?
本地搜索,
ML,
文字匹配,
文字轉(zhuǎn)拼音 匹配
Mac three
local player
Realm , AudioKit