一、前言
1、之前寫了一篇 UIView 的分類,一句代碼顯示無數(shù)據(jù)界面 ,如果針對 tableView 或者 collectionView 使用起來還是挺麻煩的,簡單分析一下吧
優(yōu)點: 適用范圍比較廣泛,只要界面是
UIView 或 其子類
,都適用-
缺點:
- 需要調(diào)用者手動管理(創(chuàng)建顯示和隱藏),使用起來不方便。
- 沒有針對無網(wǎng)絡(luò)進行封裝,需要調(diào)用者在外界自己判斷
2、先來看看本框架達到的效果吧,授人以魚不如授人以漁!簡單功能,本文做了詳細分析,開源的更多是封裝思想,所以文字比較多,請做好心理準備,但絕對有所收獲的
二、分析思考
1、實際項目中使用顯示無數(shù)據(jù)或者無網(wǎng)絡(luò)界面 ,一般都是
UITableView
或者UICollectionView
此時如果使用 UIView 的分類 相對麻煩很多,如果是給舊項目添加這個功能,修改量就很大了,因為需要手動管理顯示和隱藏2、那么如何避免手動管理呢?考慮到
UITableView
和UICollectionView
兩個都有reloadData
方法,調(diào)用一次就會重新重新執(zhí)行dataSource
數(shù)據(jù)源方法,實際項目中,我們請求網(wǎng)絡(luò)拿到列表數(shù)據(jù)后,都需要調(diào)用reloadData
,而恰恰這個時候,為了更好的用戶體驗,我們也需要處理是否無數(shù)據(jù)或者無網(wǎng)絡(luò),如果沒網(wǎng)絡(luò),需要顯示無網(wǎng)絡(luò)界面;而無數(shù)據(jù)就要顯示無數(shù)據(jù)界面,那能不能在reloadData
方法里面就處理了,或許你已經(jīng)想到了3、對的,用runtime 替換掉tableView 或者 collectionView 的
reloadData
方法,然后在替換的方法里面處理好顯示界面的邏輯,此時每當(dāng)執(zhí)行reloadData
的時候,就自動判斷需要顯示什么界面,調(diào)用者不需要手動管理4、要替換系統(tǒng)的
reloadData
方法,有兩種方式,分類和繼承,原理都一樣,本文就使用分類對UITableView 進行分析,當(dāng)然UICollectionView 也是一樣的,思路一樣,如果需要,大家可自行實現(xiàn)-
5、需要什么樣的功能
(1)參考不同的app,有些 app 顯示無數(shù)據(jù)界面是一張gif 圖,當(dāng)然主流的都是 靜態(tài)圖 ,因此必須支持靜圖和動圖的顯示
(2)圖片數(shù)據(jù)一般來自本地,但有可能來自網(wǎng)絡(luò)(后臺可以隨時更換顯示的無數(shù)據(jù)圖,更新維護相對方便),因此必須要支持網(wǎng)絡(luò)url下載,當(dāng)然,為了更好的用戶體驗,網(wǎng)絡(luò)圖片下載后都需要緩存起來,下次就不需要再請求網(wǎng)絡(luò),而且,本地的gif也需要緩存到內(nèi)存中,為了加快讀取速度,可以參考SDWebImage,內(nèi)存和沙盒都緩存起來,先從內(nèi)存中獲取,沒有再從沙盒中獲取,再沒有才請求網(wǎng)絡(luò);既然有緩存,肯定也需要清空緩存
** (3)考慮到此時可能會顯示或者隱藏
UINavigationBar
或UITabBar
,那么這個無數(shù)據(jù)或無網(wǎng)絡(luò)界面也需要動態(tài)更新布局,填充界面,不能留空白**(4)當(dāng)然還需要處理點擊事件,考慮到分類拓展性不強,因此默認是整個界面點擊,如果你是用繼承實現(xiàn),這就好辦,還可以提供自定義界面(custom view)等等,本文就不作分析了
三、API 設(shè)計
1、是否開啟緩存,默認開啟,開啟后,會緩存到沙盒 以及 內(nèi)存,如果是本地gif圖片,也會緩存到內(nèi)存
/**
* @author gitKong
*
* 是否開啟自動緩存,此時會緩存到沙盒 和 內(nèi)存中,默認開啟
*/
@property (nonatomic,assign)BOOL fl_autoCache;
2、沒有數(shù)據(jù)顯示的圖片,不能為nil(內(nèi)部有斷言),可以傳入本地圖片名 或者 網(wǎng)絡(luò)URL (包括gif,如果本地gif 圖,需要加上后綴)
/**
* @author gitKong
*
* 沒有數(shù)據(jù)顯示的圖片,不能為nil
*
* 可傳入 本地圖片名 或者 網(wǎng)絡(luò)URL (包括gif)
*/
@property (nonatomic,copy)NSString *fl_noData_image;
3、沒有網(wǎng)絡(luò)顯示的圖片,不能為nil(內(nèi)部有斷言),可以傳入本地圖片名 或者 網(wǎng)絡(luò)URL (包括gif,如果本地gif 圖,需要加上后綴)
/**
* @author gitKong
*
* 沒有網(wǎng)絡(luò)顯示的圖片,不能為nil
*
* 可傳入 本地圖片名 或者 網(wǎng)絡(luò)URL (包括gif)
*/
@property (nonatomic,copy)NSString *fl_noNetwork_image;
4、沒有網(wǎng)絡(luò)或者沒有數(shù)據(jù)顯示界面的點擊事件,默認是整個界面的點擊響應(yīng)。如果自定義需求比較大,建議使用繼承實現(xiàn)。
/**
* @author gitKong
*
* 沒有網(wǎng)絡(luò)或者沒有數(shù)據(jù)顯示界面的點擊事件
*/
- (void)fl_imageViewClickOperation:(void(^)())clickOperation;
5、清空緩存,包括沙盒 和 內(nèi)存中的都會清空,如果需要單獨清空,可以從 實現(xiàn)文件 中開放出來
/**
* @author gitKong
*
* 清空緩存(包括沙盒和內(nèi)存)
*/
- (void)fl_clearCache;
四、關(guān)鍵代碼分析
1、Swizzling方法替換,在load 方法(load是只要類所在文件被引用就會被調(diào)用)中實現(xiàn),如果方法存在那么直接替換方法,如果不存在則交換方法實現(xiàn),替換tableView的
reloadData
方法,內(nèi)部處理是否有網(wǎng)絡(luò)或者有數(shù)據(jù)顯示的界面
+ (void)fl_methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector{
Class class = [self class];
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
2、判斷網(wǎng)絡(luò)狀態(tài),考慮到如果使用
Reachability
需要導(dǎo)入文件,有一定的耦合性,不方便移植,因此本框架是通過獲取狀態(tài)欄的信息來判斷,通過 runtime && KVC 就很容易獲取狀態(tài)欄的信息(runtime 可以知道UIStatusBar
的所有屬性信息,KVC 進行屬性操作),經(jīng)測試發(fā)現(xiàn),飛行模式和關(guān)閉移動網(wǎng)絡(luò)都拿不到dataNetworkType
屬性信息,1 - 2G; 2 - 3G; 3 - 4G; 5 - WIFI
- (BOOL)checkNoNetwork{
BOOL flag = NO;
UIApplication *app = [UIApplication sharedApplication];
NSArray *children = [[[app valueForKeyPath:@"statusBar"] valueForKeyPath:@"foregroundView"] subviews];
int netType = 0;
//獲取到網(wǎng)絡(luò)返回碼
for (id child in children) {
if ([child isKindOfClass:NSClassFromString(@"UIStatusBarDataNetworkItemView")]) {
//獲取到狀態(tài)欄,飛行模式和關(guān)閉移動網(wǎng)絡(luò)都拿不到dataNetworkType;1 - 2G; 2 - 3G; 3 - 4G; 5 - WIFI
netType = [[child valueForKeyPath:@"dataNetworkType"] intValue];
switch (netType) {
case 0:
flag = NO;
//無網(wǎng)模式
break;
default:
flag = YES;
break;
}
}
}
return flag;
}
3、判斷是否有數(shù)據(jù) 直接通過
dataSource
獲取對應(yīng)的section
和row
進行判斷,只要row
不為空,那么就證明有數(shù)據(jù)
- (BOOL)checkNoData{
NSInteger sections = 1;
NSInteger row = 0;
BOOL isEmpty = YES;
if ([self.dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) {
sections = [self.dataSource numberOfSectionsInTableView:self];
}
for (NSInteger section = 0; section < sections; section++) {
if ([self.dataSource respondsToSelector:@selector(tableView:numberOfRowsInSection:)]) {
row = [self.dataSource tableView:self numberOfRowsInSection:section];
if (row) {
// 只要有值都不是空
isEmpty = NO;
}
else{
isEmpty = YES;
}
}
}
return isEmpty;
}
4、判斷NavigationBar和TabBar 顯示隱藏,更新界面的布局,填充不留空白
- 判斷NavigationBar (提供三種方案)
- 通過
runtime
發(fā)現(xiàn)UITableView
有 一個隱藏屬性visibleBounds
,直譯過來就是可視區(qū)域,通過實測,如果沒有導(dǎo)航控制器,那么visibleBounds
的y = 0
,如果有導(dǎo)航控制器,而且UINavigationBar
是顯示,那么y = -64
,如果有導(dǎo)航控制器,但UINavigationBar
隱藏,那么y = -20
可以通過這個來判斷導(dǎo)航欄是否隱藏; - 當(dāng)然這個確實麻煩點,可以使用 我之前的文章 任意NSObject及其子類中獲取當(dāng)前顯示的控制器 此時可以獲取當(dāng)前顯示的控制器,然后判斷NavigationBar 顯示隱藏
- 當(dāng)然,還有一種辦法,不需要去手動判斷,
UITableView
有 還有一個隱藏屬性wrapperView
這個 view 可以在debug view Hieratrchy
里面看到層級結(jié)構(gòu),通過實測,這個會隨著導(dǎo)航欄顯示隱藏 來改變 y 的偏移,因此直接將無數(shù)據(jù)或者無網(wǎng)絡(luò)頁面添加到wrapperView
上就可以了 - 判斷TabBar:本來打算通過
[UITabBar appearance]
來獲取,發(fā)現(xiàn)雖然不會報錯,但測試發(fā)現(xiàn)沒任何效果,通過斷點po提示<_UIAppearance:0x17025b000> <Customizable class: UITabBar> with invocations (null)>
是空的,不能獲取到,當(dāng)然[UINavigationBar appearance]
也沒效果,所以此時使用 任意NSObject及其子類中獲取當(dāng)前顯示的控制器 來判斷TabBar是否顯示
- (void)updataImageViewFrame{
// 如果沒有導(dǎo)航控制器,那么rect的y值為0,如果有導(dǎo)航控制器,那么y為-64,如果導(dǎo)航控制器hidden那么也會跟著變,不需要額外修改
Class conecreteValue = NSClassFromString(@"NSConcreteValue");
id concreteV = [[conecreteValue alloc] init];
concreteV = [self valueForKey:@"visibleBounds"];
CGRect rect ;
[concreteV getValue:&rect];
// 判斷是否有tabBar顯示
// 注意:分類中使用[UITabBar appearance] 和 [UINavigationBar appearance] 都不能獲取對象,斷點po提示<_UIAppearance:0x17025b000> <Customizable class: UITabBar> with invocations (null)>
UIViewController *currentVc = [self fl_viewController];
UITabBarController *tabVc = (UITabBarController *)currentVc.tabBarController;
if (tabVc) {
self.imageView.frame = CGRectMake(rect.origin.x, 0, rect.size.width, rect.size.height + rect.origin.y - (tabVc.tabBar.hidden ? 0 : tabVc.tabBar.bounds.size.height));
}
else{
self.imageView.frame = CGRectMake(rect.origin.x, 0, rect.size.width, rect.size.height + rect.origin.y);
}
}
5、獲取GIF 圖片 每一幀播放時長,通過一個key
kCGImagePropertyGIFUnclampedDelayTime
可以獲取,然后拼接起來,播放GIF 圖片
- (CGFloat)durationWithSource:(CGImageSourceRef)source atIndex:(NSUInteger)index {
float duration = 0.1f;
CFDictionaryRef propertiesRef = CGImageSourceCopyPropertiesAtIndex(source, index, nil);
NSDictionary *properties = (__bridge NSDictionary *)propertiesRef;
NSDictionary *gifProperties = properties[(NSString *)kCGImagePropertyGIFDictionary];
NSNumber *delayTime = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime];
if (delayTime) duration = delayTime.floatValue;
else {
delayTime = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime];
if (delayTime) duration = delayTime.floatValue;
}
CFRelease(propertiesRef);
return duration;
}
五、總結(jié)
1、加載GIF 圖片內(nèi)存占用挺大,特別是緩存到內(nèi)存中,內(nèi)存會飆升,注意使用,測試發(fā)現(xiàn)
SDWebImage
也會出現(xiàn)內(nèi)存飆升,YYImageCache
的話就優(yōu)化很多,待優(yōu)化2、分類中使用
[UITabBar appearance]
和[UINavigationBar appearance]
都不能獲取對象,斷點po提示<_UIAppearance:0x17025b000> <Customizable class: UITabBar> with invocations (null)>
3、因為判斷網(wǎng)絡(luò)是通過獲取狀態(tài)欄信息來判斷,如果 是 CMCC 連接的WI-FI,就不能正確判斷網(wǎng)絡(luò)是否已聯(lián)網(wǎng)
4、此框架零耦合,方便移植,使用方便,只需要設(shè)置
fl_noData_image
和fl_noNetwork_image
,只要調(diào)用reloadData
就會自動判斷需要顯示什么界面4、上文中提到的功能點都實現(xiàn)了,簡單的功能,但做了詳細的分析,從需求確定-功能分析-技術(shù)實現(xiàn)都做了詳細的分析,封裝的思想才是關(guān)鍵,開源不單單是代碼,更多的是封裝的思想
5、具體實現(xiàn)代碼比較多,本文就不一一詳細講解,Github Demo 中有 對應(yīng)的注釋,歡迎大家關(guān)注我,喜歡給個like 和 star,會隨時開源~