老規(guī)矩窄赋,一圖勝千言
先嘮叨幾句
最近也是忙于項目進(jìn)度,學(xué)習(xí)時間被大大的壓縮了下來,距離上次寫文章已經(jīng)過去了整整倆月時間。項目進(jìn)度已經(jīng)趕的差不多了所以抽空將項目中遇到的問題及解決方法記錄一下。
出現(xiàn)的問題
隨著項目漸漸的接近尾聲,在項目中列表無數(shù)據(jù)展示成為了令人頭疼的問題欢摄。如何優(yōu)雅且更智能的讓占位圖在列表無數(shù)據(jù)時自動展示出來,我剛開始的做法是將圖片和提示文字寫在一個自定義 view 上笋粟,每次當(dāng)網(wǎng)絡(luò)請求完成都要去判斷當(dāng)前列表是否有數(shù)據(jù)怀挠,如果沒有數(shù)據(jù)則將 view 加載到列表中;如果有數(shù)據(jù)則將 view 隱藏掉害捕。這樣暴露出很嚴(yán)重的問題是:每個列表對應(yīng)一個相應(yīng)的自定義展示 view 對象绿淋,如果一個頁面有好幾個列表,那么對自定義展示圖的控制就沒難么容易了尝盼,而且每次都要計算占位圖的frame
或者當(dāng)上拉加載更多時沒有請求到數(shù)據(jù)并且當(dāng)前列表中有上次請求的數(shù)據(jù)吞滞,如果用當(dāng)前請求的數(shù)據(jù)去判斷 view 的隱藏與否是不正確的,所以還要拿到當(dāng)前列表的總數(shù)據(jù)去判斷。
我曾在網(wǎng)上找到了一個很優(yōu)秀的三方框架DZNEmptyDataSet 下載下來看了一下不是很符合自己的要求所以并沒有將其放入自己的項目中(有興趣的同學(xué)可以下載試玩一下)裁赠。在12月21日那天在掘金網(wǎng)上無意瀏覽了一個博客感覺很棒殿漠,很符合自己的需求所以就按照博主的 Demo 重寫并優(yōu)化了一下用在了自己的項目中,下面對demo 中的部分代碼進(jìn)行講解佩捞。
說一說 category
我在項目中喜歡用分類 為控制器绞幌、view 或者 NSObject 類型等等擴(kuò)展屬性和方法,這樣既不與別人寫的代碼沖突而且實現(xiàn)起來也更優(yōu)雅一忱。說起 category 必定與 runtime 密不可分莲蜘,對 runtime 的使用我其實也只會一點點而且大部分都是什么時候用什么時候查,好了帘营,回歸正題票渠。
部分代碼講解
重寫+(void)load
方法,我們在平時寫自定 view 時都會在.m 中重寫一下-(instancetype)init
或者- (instancetype)initWithFrame:(CGRect)frame
方法來初始化控件芬迄,而tableview
或者collectionView
在reloaddata
時也會調(diào)用load
方法问顷。如果在列表 reload 的時候?qū)α斜韮?nèi)的數(shù)據(jù)進(jìn)行檢測來達(dá)到是否展示占位圖的效果,可所謂是一舉兩得啊薯鼠。
一言不合就貼一手代碼。
+(void)load{
//為了保證該對象的實例化方法只交換一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method reloadData = class_getInstanceMethod(self, @selector(reloadData));
Method m_reloadData = class_getInstanceMethod(self, @selector(m_reloadData));
method_exchangeImplementations(reloadData, m_reloadData);
Method delloc = class_getInstanceMethod(self, NSSelectorFromString(@"dealloc"));
Method m_delloc = class_getInstanceMethod(self, @selector(m_delloc));
method_exchangeImplementations(delloc, m_delloc);
});
}
從代碼中主要用到runtime兩種方法:1.獲取當(dāng)前對象實例化方法class_getInstanceMethod
2.方法交換method_exchangeImplementations
械蹋;獲取的方法分別是reloadData
和delloc
這兩種方法出皇,獲取delloc
方法主要是為了移除監(jiān)聽,下面再說這個方法哗戈。來看一下reload
方法的實現(xiàn):
-(void)m_reloadData{
[self m_reloadData];
//第一次忽略,不展示占位圖
if (!self.isInitFinish) {
[self m_havingData:YES];
self.isInitFinish = YES;
return;
}
// 刷新完成之后檢測數(shù)據(jù)量
dispatch_async(dispatch_get_main_queue(), ^{
NSInteger numberOfSections = [self numberOfSections];
//如果沒有數(shù)據(jù)則調(diào)用底下方法來創(chuàng)建占位圖
BOOL havingData = NO;
for (NSInteger i = 0; i < numberOfSections; i++) {
if ([self numberOfRowsInSection:i] > 0) {
havingData = YES;
break;
}
}
[self m_havingData:havingData];
});
}
不知道大家看完這個方法是不是腦子中已經(jīng)浮現(xiàn)出創(chuàng)建占位圖的大致邏輯了郊艘,此方法中還有一個小小的彩蛋不知道大家注意到?jīng)]有,因為對 runtime 的不了解唯咬,反正我當(dāng)時看的時候沒有注意到纱注,后來才發(fā)現(xiàn)這一點:在上面那個方法中有沒有注意到第一行代碼[self m_reloadData];
在自己中調(diào)用自己?這不就成死循環(huán)了嗎胆胰?我剛開始也并不理解狞贱,我也并沒有去查資料,我自己的理解是這樣的:在reload
里面利用method_exchangeImplementations
方法將 tableview 的reload
和m_reloadData
進(jìn)行交換蜀涨,每次獲取到數(shù)據(jù)去刷新列表時reloadData方法進(jìn)行的動態(tài)替換瞎嬉,也就是說調(diào)用 tableview 的reloadData
實則調(diào)用的是m_reloadData
,而m_reloadData
做的主要工作就是控制占位圖的顯示與隱藏并沒有去刷新列表厚柳,那列表怎么刷新把踉妗?那就是調(diào)用m_reloadData
實則是調(diào)用的是列表的reloadData
别垮,所以才會出現(xiàn)上面方法中所寫的方法便监。是不是有點繞碳想,上面所述純粹個人見解。
如何讓不同列表有不同占位圖
在創(chuàng)建 tableview 或 collectionView 時老充,實現(xiàn)與之對應(yīng)的代理是必不可少的一步啡浊。那代理有沒有可能幫到我們呢巷嚣?答案是肯定的钳吟。說一句題外話:入行有段時間了红且,漸漸地對代碼有了新的認(rèn)識嗤放,一個 app 的構(gòu)成就是內(nèi)部收發(fā)消息壁酬,無論你干什么你都需要將消息傳出去舆乔,接收消息,反饋消息吊宋,請仔細(xì)想想無論代碼世界還是現(xiàn)實生活贫母,消息的機(jī)制被用到萬事萬物中腺劣¢僭回正題趾断,說了句題外話的目的就是為了說明 app 內(nèi)無論是收消息還是找消息都是通過Selector
去做的芋酌,我們可以利用 tableview 的代理對象來達(dá)到這個目的脐帝。
來看代碼
@protocol MTableViewDelegate <NSObject>
//如果不在當(dāng)前類中聲明這些方法炸站,當(dāng)用 self.delgate 查找這些方法時會出現(xiàn)黃色的警告
@optional
- (UIView *)m_noDataView; //完全自定義占位圖
- (UIImage *)m_noDataViewImage; //使用默認(rèn)占位圖, 提供一張圖片, 可不提供, 默認(rèn)不顯示
- (NSString *)m_noDataViewMessage; //使用默認(rèn)占位圖, 提供顯示文字, 可不提供, 默認(rèn)為暫無數(shù)據(jù)
- (UIColor *)m_noDataViewMessageColor; //使用默認(rèn)占位圖, 提供顯示文字顏色, 可不提供, 默認(rèn)為灰色
- (NSNumber *)m_noDataViewCenterYOffset; //使用默認(rèn)占位圖, CenterY 向下的偏移量
@end
我們在分類.m 中寫上這些方法旱易,然后利用UITableViewDelegate
去檢測這些方法是否存在
//判斷是否響應(yīng)圖片代理
BOOL isImg = [self.delegate respondsToSelector:@selector(m_noDataViewImage)];
請注意這里的self.delegate
這句代碼檢測的已經(jīng)不是當(dāng)前類中的方法了阀坏,而是當(dāng)你初始化 tableview 時將xxx.delegate = self;
這樣賦值是代理的對象已經(jīng)是當(dāng)前類了忌堂,所以這個方法是否響應(yīng)浸船,檢索的方法應(yīng)該是你所賦值的類中。我們常說的一句話就是面向?qū)ο?/strong>登淘,我認(rèn)為:類也是對象,類的 category 也是一個對象耍鬓,類與對象沒什么區(qū)別流妻,類是對象的抽象化绅这,對象是類的具體化,具體的事物是對象, 將具有相同或相似性質(zhì)的對象的屬性或方法抽象出來便是類匆篓。如果你分不清什么是類什么是對象鸦概,那么你在寫代碼的時候肯定會遇到不必要的麻煩甩骏。
我們知道 tableview 有個屬性叫backgroundView
谨设,我們可以很巧妙的將自定義的占位視圖給這個屬性缎浇,反正平時這個屬性大家也不是很常用二蓝。當(dāng)你將自定義視圖給self.backgroundView = xxx
時指厌,發(fā)現(xiàn)你滾動列表時backgroundView
是固定不動的鸥诽,那有什么辦法能讓視圖跟著列表一起滾動起來牡借,可以設(shè)置監(jiān)聽钠龙,監(jiān)聽 tableview 的frame
碴里,在 tableview 滾動contentOffset
改變時, backgroundView 的frame.origin.y
也是同步改變的, 所以我們看起來無論 TableView 怎么滾動占位圖都是無動于衷的, 如果我們想讓占位圖跟隨滾動的話, 只要取消掉backgroundView 的 frame.origin.y
的同步更新就好了, 也就是說要保證 frame.origin.y
的值一直為0咬腋,具體的可以看下 demo 實現(xiàn)根竿。設(shè)置監(jiān)聽必定需要移除監(jiān)聽蠢壹,如果不在delloc
中移除監(jiān)聽的話图贸,由于監(jiān)聽會一直存在必定造成崩潰疏日,所以才動態(tài)的去替換delloc
方法沟优。我在 demo 中并沒有去移除監(jiān)聽挠阁,而是在NSObject+MAdd
這個文件中調(diào)用了
- (void)m_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
這個方法溯饵,此方法可在對象消失后自動移除監(jiān)聽隘谣。具體實現(xiàn)詳見 Demo寻歧。
不想要占位圖?
如果在實現(xiàn) tableview 的代理文件中逗概,不實現(xiàn)上述的幾個方法就不會加載占位圖弟晚,demo 已在這一部分做了處理忘衍。其實逾苫,你還可以為占位視圖添加些許方法:提示文字的字體大小、提示文字的富文本屬性枚钓、加載圖片的動畫等等铅搓,這些需要自行實現(xiàn)。
列表有tableHeaderView
怎么辦搀捷?
有的列表是有tableHeaderView
的情況下星掰,上面的方法是行不通的多望,因為tableHeaderView如果過高的情況下會把backgroundView
蓋住,導(dǎo)致占位圖無法顯示氢烘,有興趣的小伙伴可以試一下我說的這種情況蜀踏。我的做法是:如果tableHeaderView
的高度超過了當(dāng)前tableview高度的一半時將占位圖加載到tableFooterView
上,前提是當(dāng)前 tableview 的 fotterview 沒有內(nèi)容钳榨。如果高度沒有超過一半則還加載到backgroundView
上昭卓。如果你有更好的方法請及時聯(lián)系我伙菊。
聊一聊 runtime 的簡單用法
在我上一篇關(guān)于列表的預(yù)加載文章中簡單敘述了一下關(guān)于 runtime 的基本用法兴枯,我現(xiàn)在將里面的細(xì)節(jié)再一一扣一下躺坟。
在寫 category 類的分類時匣摘,經(jīng)常為現(xiàn)有的類添加私有變量或者為現(xiàn)有的類添加共有屬性供外部訪問擦囊。寫過分類的朋友都知道在寫分類屬性的時候涧郊,編譯器會給出一個黃色警告幌陕,比如我為某個分類創(chuàng)建一個為test
屬性會報如下警告:
Property 'test' requires method 'setTest:' to be defined
- use @dynamic or provide a method implementation in this category
簡單翻譯一下這段話:test
屬性,必須實現(xiàn)setTest:
或?qū)⑵錁?biāo)記為@dynamic
或者在此類里提供方法實現(xiàn)失暴,即 get 方法现恼。
@dynamic 是什么喳逛?
我們都知道當(dāng)你@property
一個屬性時,編譯器會自動給你實現(xiàn)setter
和getter
方法砖织,自動為你實現(xiàn)方法的為@synthesize
,而與之對應(yīng)的則是@dynamic
块请。當(dāng)一個屬性被標(biāo)記為@dynamic
時海渊,此時編譯器就認(rèn)為該屬性的 setter和 getter 方法由用戶自己實現(xiàn)哲鸳,不自動生成婿奔。如果該屬性被標(biāo)記為@dynamic
就算沒有實現(xiàn) setter 和 getter 方法編譯也會通過,如果當(dāng)程序運行到xxx.test = xxx
時挖腰,由于缺少與其相對應(yīng)的 setter 方法導(dǎo)致崩潰翰蠢;或者xxx *pro = test
時,由于缺少 getter 方法同樣會導(dǎo)致崩潰梁沧。在編譯時沒有問題檀何,運行時才執(zhí)行相應(yīng)的方法,這就是動態(tài)綁定,即 runtime 運行時频鉴。
在分類中實現(xiàn) setter 和 getter 方法是用 runtime 中的objc_setAssociatedObject
和objc_getAssociatedObject
栓辜,來看一下實現(xiàn)方法
-(void)setTest:(NSString *)test{
objc_setAssociatedObject(self, @selector(test), test, OBJC_ASSOCIATION_RETAIN);
}
-(NSString *)test{
return objc_getAssociatedObject(self, _cmd);
}
在objc_setAssociatedObject會涉及到四個參數(shù),分別是object
砚殿、key
啃憎、value
芝囤、policy
1.object
似炎,要關(guān)聯(lián)的對象即 self
2.key
,這個 key 值必須保證是一個對象級別的唯一常量與創(chuàng)建 tablviewcell 所創(chuàng)建的 ID 類似悯姊;
一般來說羡藐,有以下三種推薦的 key 值:
① 聲明static const char * key_m_test = "key_m_test"
;使用 &key_m_test
作為 key 值這個是需要加&符號獲取地址;
② 聲明 static const void * key_m_test = "key_m_test"
,使用key_m_test
作為 key 值悯许;以上兩種寫法我認(rèn)為是一個是 C 寫法仆嗦,一個為 OC 寫法
③ 用 selector ,使用 getter 方法的名稱作為 key 值先壕。因為它省掉了一個變量名瘩扼,非常優(yōu)雅地解決了命名問題。
3.value
即當(dāng)前屬性的值
4.policy
關(guān)聯(lián)策略
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0,//弱引用對象
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, //強(qiáng)引用對象且為非原子操作
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,//復(fù)制關(guān)聯(lián)對象且為非原子操作
OBJC_ASSOCIATION_RETAIN = 01401,//強(qiáng)引用對象且為原子操作
OBJC_ASSOCIATION_COPY = 01403//復(fù)制關(guān)聯(lián)對象為原子操作
};
將前三種翻譯過來即:
@property (nonatomic, assign)
@property (nonatomic, strong)
@property (nonatomic, copy)
細(xì)心的同學(xué)會發(fā)現(xiàn)垃僚,在寫 getter 方法是用到了一個_cmd
集绰,自己寫一下發(fā)現(xiàn)他是一個SEL
類型,這個_cmd
是什么谆棺?以下為我查閱的資料:
Objective-C中的方法默認(rèn)被隱藏了兩個參數(shù):self
和_cmd
栽燕。self指向?qū)ο蟊旧恚琠cmd指向方法本身改淑。舉兩個例子來說明:
例一:
- (NSString *)name
這個方法實際上有兩個參數(shù):self和_cmd碍岔。
例二:
- (void)setValue:(int)value
這個方法實際上有三個參數(shù):self, _cmd和value。被指定為動態(tài)實現(xiàn)的方法的參數(shù)類型有如下的要求:
A.第一個參數(shù)類型必須是id(就是self的類型)
B.第二個參數(shù)類型必須是SEL(就是_cmd的類型)
C.從第三個參數(shù)起朵夏,可以按照原方法的參數(shù)類型定義蔼啦。
舉兩個例子來說明:
例一:setHeight:(CGFloat)height
中的參數(shù)height是浮點型的,所以第三個參數(shù)類型就是f仰猖。
例二:再比如setName:(NSString *)name
中的參數(shù)name是字符串類型的询吴,所以第三個參數(shù)類型就是@類型
有一句代碼是xxx.name = @"xxx";程序運行到這里時,會去.m中尋找setName:
這個賦值方法亮元。但是.m里并沒有這個方法猛计,于是程序進(jìn)入methodSignatureForSelector:
中進(jìn)行消息轉(zhuǎn)發(fā)。執(zhí)行完之后爆捞,以"v@:@"
作為方法簽名類型返回奉瘤。
這里v@:@
是什么東西呢?實際上,這里的第一個字符v代表函數(shù)的返回類型是void盗温,(后面三個字符分別self, _cmd, name這三個參數(shù)的類型id, SEL, NSString藕赞。
接著程序進(jìn)入forwardInvocation
方法。得到的key為方法名稱setName:
卖局,然后利用[invocationgetArgument:&obj atIndex:2];
獲取到參數(shù)值斧蜕,這里是“xxx”。這里的index為什么要取2呢砚偶?如前面分析批销,第0個參數(shù)是self,第1個參數(shù)是_cmd染坯,第2個參數(shù)才是方法后面帶的那個參數(shù)均芽。
最后利用一個可變字典來賦值。這樣就完成了整個setter過程单鹿。
有一句代碼是 NSLog(@"%@", xxx.test);掀宋,程序運行到這里時,會去.m中尋找name這個取值方法 仲锄。但是.m里并沒有這個取值方法劲妙,于是程序進(jìn)入methodSignatureForSelector:
中進(jìn)行消息轉(zhuǎn)發(fā)。執(zhí)行完之后儒喊,以"@@:"
作為方法簽名類型返回镣奋。這里第一字符@
代表函數(shù)返回類型NSString,第二個字符@
代表self的類型id澄惊,第三個字符:
代表_cmd的類型SEL唆途。
接著程序進(jìn)入forwardInvocation
方法。得到的key為方法名稱name掸驱。
最后根據(jù)這個key從字典里獲取相應(yīng)的值肛搬,這樣就完成了整個getter過程。
以上是 runtime 在賦值與取值做的整個流程毕贼,這些資料我也是在網(wǎng)上找的自己對其流程也知之甚少温赔,希望與之共進(jìn)步。
總結(jié)
知其然鬼癣,知其所以然玉锌。做技術(shù)需要有一絲不茍的精神沟蔑,曾同事說過這么一段話:不要以為將別人的東西粘貼復(fù)制過來混萝,改改名字就變成了自己的東西了妖混。這句話也令我反思,是啊章郁,現(xiàn)在搞技術(shù)都太浮躁枉氮,無論 demo 是如何實現(xiàn)的志衍,用到了哪些知識從不關(guān)心,符合自己需求的直接粘貼復(fù)制過來聊替,我想這種做法就違背了寫 demo 人的根本意圖了楼肪。仰望星空的同時一定要腳踏實地,我將與你一路同行惹悄。