UITableView佔點陣圖的低耦合性設計

簡品發表於2018-04-08

緣由

基於物件導向的開發原則中的迪米特法則:一個軟體實體應當儘可能少的與其他實體發生相互作用;為了降低列表無資料佔點陣圖的使用成本及程式碼耦合性,對網上現用的一些解決方案加以優化;

核心

針對基於runtime替換reloadData方法的相關,這裡就不做多闡述了,本文主要討論以下幾個問題:

  • 1.需要顯示佔點陣圖的情況;
  • 2.tableView初次系統呼叫reloadData方法的干擾排除最優方案;
  • 3.網路因伺服器故障請求失敗的處理;
  • 4.佔點陣圖觸發再次網路請求的策略;

問題1:需要顯示佔點陣圖的情況

現在流行的判斷方案是:

  • tableView.rows==0;

我需要補充說明的是:

  • tableView.sections>0&&tableView.rows==0&&tableView.viewForHeaderInSection!=nil;

針對第一種rows==0的情況就不做多解釋;第二種的話主要就是:當一個列表的資料繫結在sectionHeaderView上面,此時row==0;然後需求是:點選sectionHeaderView,展開section,重新整理資料;row>=0;所以如果僅僅考慮rows==0的情況,在第二種需求的情況佔點陣圖顯示就會異常;

補充:無網路的時候直接載入佔點陣圖;

問題2:tableView初次系統呼叫reloadData方法的干擾排除最優方案

在網上我看到的解決方案是:
在category給UITableView新增isFirstReload屬性;如果是第一次載入的話設定tableView.isFirstReload = YES;然後內部的判斷是:

if (!self.firstReload) {
    [self checkEmpty];
}
self.firstReload = NO;
複製程式碼

針對每次都需要在控制器中呼叫tableView.isFirstReload = YES,我也是做了很多優化,比如最開始的時候我會想直接在基類viewDidLoad或者利用Aspects切入viewWillApear``方法中:遍歷子檢視,如果是[UITableView Class]或者[UICollectionView Class]就直接呼叫;

- (void)aspectViewWillAppearWithViewController:(UIViewController *)viewController
{
    NSArray *subViews = viewController.view.subviews;
    [subViews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([view isKindOfClass:[UITableView class]]) {
            UITableView *tableView = (UITableView *)view;
            if (!tableView.isNotFirstReload) {
                tableView.isNotFirstReload = NO;
            }
        }
        //如果tableView非self.view的直接子檢視,而是孫檢視....  
        //可用遞迴優化;
        NSArray *secondLevelSubviews = view.subviews;
        [secondLevelSubviews enumerateObjectsUsingBlock:^(UIView *secondView, NSUInteger idx, BOOL * _Nonnull stop) {
            if ([secondView isKindOfClass:[UITableView class]]) {
                UITableView *tableView = (UITableView *)secondView;
                if (!tableView.isNotFirstReload) {
                    tableView.isNotFirstReload = NO;
                }
            }
            NSArray *thirdLevelSubviews = secondView.subviews;
            [thirdLevelSubviews enumerateObjectsUsingBlock:^(UIView *thirdView, NSUInteger idx, BOOL * _Nonnull stop) {
                if ([thirdView isKindOfClass:[UITableView class]]) {
                    UITableView *tableView = (UITableView *)thirdView;
                    if (!tableView.isNotFirstReload) {
                        tableView.isNotFirstReload = NO;
                    }
                }
            }];
        }];
    }];
}
複製程式碼

如此優化,是可以達到效果,但是在檢視啟動的時候遍歷子檢視無非是效能耗損的;最後腦筋急轉彎,其實也就是一個很簡單的方法就能解決這個問題:

@property (nonatomic, assign) BOOL isNotFirstReload;  
複製程式碼
if (self.isNotFirstReload) {
    [self checkEmpty];
}
self.isNotFirstReload = YES;  
複製程式碼

BOOL屬性第一次載入的時候本來就是NO,也就避免了外部的傳入;

問題3:網路因伺服器故障請求失敗的處理

也就是在網路請求的時候走failure的時候;一般情況下,在控制器失敗的回撥中我們不會手動呼叫[self.taleView reloadData];如果不呼叫的話,就不能正確的載入佔點陣圖了;當然你也可以在失敗的回撥中呼叫reloadData方法解決這個問題;我這裡給出另外一種解決方案:

通過windowrootViewController拿到當前的控制器,然後通過遍歷當前控制器的子檢視獲取tableView,呼叫reloadData方法,主要程式碼如下:

	+ (instancetype)shareInstance
{
    static RequestFailureHandler *shareInstance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        shareInstance = [[RequestFailureHandler alloc] init];
    });
    return shareInstance;
}
- (void)handleRequestFailure
{
    //根控制器是UINavigationController
    if ([self.rootVC isKindOfClass:[UINavigationController class]]) {
        [[RequestFailureHandler shareInstance] handleWithNavgationController:(UINavigationController *)self.rootVC];
    }else
    {
        //沒有UINavigationController的情況下
        [[RequestFailureHandler shareInstance] findTargetViewWithController:self.rootVC];
    }
}
- (void)handleWithNavgationController:(UINavigationController *)nav
{
    UIViewController *vc = nav.visibleViewController;
    if (vc.childViewControllers.count>0) {
        
        if ([vc.childViewControllers.firstObject isKindOfClass:[UIPageViewController class]]) {
            UIPageViewController *pageVc = (UIPageViewController *)vc.childViewControllers.firstObject;
            UIViewController *pageChild = pageVc.viewControllers.firstObject;
            [[RequestFailureHandler shareInstance] findTargetViewWithController:pageChild];
        }
    }else{
        [[RequestFailureHandler shareInstance] findTargetViewWithController:vc];
    }
}
- (void)findTargetViewWithController:(UIViewController *)viewController
{
    NSArray *subViews = viewController.view.subviews;
    [subViews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([view isKindOfClass:[UITableView class]]) {
            UITableView *tableView = (UITableView *)view;
            [tableView reloadData];
        }
    }];
}
#pragma mark - Getters & Setters
- (UIViewController *)rootVC
{
    if (!_rootVC) {
        _rootVC = [[[UIApplication sharedApplication] delegate] window].rootViewController;
    }
    return _rootVC;
}
複製程式碼

針對這個做法,子檢視的遍歷,效能是會耗損的;但是考慮到這個主要是請求失敗的回掉中(不像在問題2中是在控制器啟動的時候);耗損也不會影響其他,並且能夠統一處理;所以湊合能用;

補充:後面突然這個方案存在一個問題:那就是當一個介面存在多個請求的時候,其中任何一個請求失敗會干擾佔點陣圖的載入;暫時沒想到更好的解決辦法;

問題4:佔點陣圖觸發再次網路請求的策略

事件回撥:

  • block回撥
  • delegate回撥

可以直接在每個控制器中接收回撥,並完成再次請求;我在這裡想的在基類懶載入tableView物件,然後設定代理接收回撥;在回撥裡面呼叫網路請求的統一方法;

- (void)loadData
{
   //子類重寫這個方法,並且在這個方法中進行網路請求
}

#pragma mark - ReRequesDataDelegate
- (void)reRequesData
{
    [self loadData];
}

- (UITableView *)tableView
{
    if (!_tableView) {
        _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height)];
        _tableView.tableFooterView = [UIView new];
        _tableView.dataSource = self;
        _tableView.delegate = self;
        _tableView.reRequestDelegate = self;
    }
    return _tableView;
}   
複製程式碼

這樣的話,子類只需要重寫loadData;並在裡面執行網路請求,就可以達到目的;

github

更多精彩:

軟體化ESJsonFormat外掛,脫離Xcode環境執行
iOS_K線三方庫

相關文章