無數(shù)據(jù)、無網(wǎng)絡(luò)界面【更多的是開源思想】

一、前言

1、之前寫了一篇 UIView 的分類,一句代碼顯示無數(shù)據(jù)界面 ,如果針對 tableView 或者 collectionView 使用起來還是挺麻煩的,簡單分析一下吧

  • 優(yōu)點: 適用范圍比較廣泛,只要界面是 UIView 或 其子類 ,都適用

  • 缺點:

    • 需要調(diào)用者手動管理(創(chuàng)建顯示和隱藏),使用起來不方便。
    • 沒有針對無網(wǎng)絡(luò)進行封裝,需要調(diào)用者在外界自己判斷

2、先來看看本框架達到的效果吧,授人以魚不如授人以漁!簡單功能,本文做了詳細分析,開源的更多是封裝思想,所以文字比較多,請做好心理準備,但絕對有所收獲的

無網(wǎng)絡(luò)顯示界面,動態(tài)適配
無數(shù)據(jù)顯示界面,動態(tài)適配

二、分析思考

  • 1、實際項目中使用顯示無數(shù)據(jù)或者無網(wǎng)絡(luò)界面 ,一般都是 UITableView 或者 UICollectionView 此時如果使用 UIView 的分類 相對麻煩很多,如果是給舊項目添加這個功能,修改量就很大了,因為需要手動管理顯示和隱藏

  • 2、那么如何避免手動管理呢?考慮到 UITableViewUICollectionView 兩個都有 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)考慮到此時可能會顯示或者隱藏 UINavigationBarUITabBar ,那么這個無數(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)的 sectionrow 進行判斷,只要 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)航控制器,那么 visibleBoundsy = 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_imagefl_noNetwork_image ,只要調(diào)用 reloadData 就會自動判斷需要顯示什么界面

  • 4、上文中提到的功能點都實現(xiàn)了,簡單的功能,但做了詳細的分析,從需求確定-功能分析-技術(shù)實現(xiàn)都做了詳細的分析,封裝的思想才是關(guān)鍵,開源不單單是代碼,更多的是封裝的思想

  • 5、具體實現(xiàn)代碼比較多,本文就不一一詳細講解,Github Demo 中有 對應(yīng)的注釋,歡迎大家關(guān)注我,喜歡給個like 和 star,會隨時開源~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,025評論 25 708
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,198評論 4 61
  • 一.今天課堂老師主要講了 switch 多分支語句。 結(jié)構(gòu): switch(條件表達式) { case 常量表達式...
    李響lx閱讀 212評論 0 0
  • [連載]我們離婚了--目錄頁 我順從地跟著林清出了大樓,然后坐了一輛出租車趕往機場。出租車司機是一位40多歲的中年...
    廉子閱讀 373評論 0 3
  • 回到北京一個月了,最近晚上加班很多,回家的時候已經(jīng)接近11點,回去的路上有一段沒有路燈,路兩側(cè)的槐樹已經(jīng)有十年樹齡...
    京城逍遙客閱讀 179評論 0 3