用runtime優化tableView寫法

Climb發表於2019-01-15

從一張圖開始:

用runtime優化tableView寫法

按模組資料繫結顯示樣式

像這種介面,佈局會比較複雜,每一section的頭尾顯示及點選事件都是動態的,而且裡面的內容是幾個row也不確定,這個介面邏輯整理後是:

NoDataCell-相關商城-相關醫生-相關醫院-相關問答-相關資訊-微脈好物-熱門醫生-熱門醫院-熱門問答-熱門資訊
複製程式碼

然後就是設定11個section,每個section裡有三個cell,通用的titleSectionCell和moreSectionCell,還有每個section對應的自己的內容cell,每個cell負責自己的UI顯示及點選事件。

整理後 邏輯思路是清晰了,但是怎麼寫程式碼呢? 如果按照思路說的這樣每個section都得判斷去寫對應的三個cell還包括個字賦值及點選事件,想一想cellForRow裡程式碼,額。。得寫多大一坨啊?

是不是可以更有效更優雅的寫這段程式碼呢? 想一下這段裡的共性,通用的頭尾好處理,內容cell的個數由各自的陣列控制 cell的選擇和index有關,而且整個tableView佈局是固定的.

所以我們可以這樣:

//排版方式:相關商品>醫生>醫院>問答>疾病標籤>資訊>熱門商品>醫生>醫院>問答>資訊
    NSDictionary *mallSectionDic     = @{kCellTitle:@"相關商品", kCellHasMore:@(self.mallHasMore), kCellHasResult:@(self.mallHasResult), kCellClass:@"WMSearchResultMallCell", kCellTitleMoreSelector:@"relativeMallMoreClick", kCellDataArray:self.resultMallArrray, kCellMoreText:@"檢視更多商品"};
    NSDictionary *doctorSectionDic   = @{kCellTitle:@"相關醫生", kCellHasMore:@(self.doctorHasMore), kCellHasResult:@(self.doctorHasResult), kCellClass:@"WMSearchResultDoctorCell", kCellTitleMoreSelector:@"relativeDoctorMoreClick", kCellDataArray:self.resultDoctorArrray, kCellMoreText:@"檢視更多醫生"};
    NSDictionary *hospitalSectionDic = @{kCellTitle:@"相關醫院", kCellHasMore:@(self.hospitalHasMore), kCellHasResult:@(self.hospitalHasResult), kCellClass:@"WMSearchResultHospitalCell", kCellTitleMoreSelector:@"relativeHospitalMoreClick", kCellDataArray:self.resultHospitalArrray, kCellMoreText:@"檢視更多醫院"};
    NSDictionary *qaSectionDic       = @{kCellTitle:@"相關問答", kCellHasMore:@(self.QAHasMore), kCellHasResult:@(self.QAHasResult), kCellClass:@"WMSearchResultQACell", kCellTitleMoreSelector:@"relativeQAMoreClick", kCellDataArray:self.resultQAArrray,kCellMoreText:@"檢視更多問答"};
//    NSDictionary *sicknessSectionDic  = @{kCellTitle:@"疾病", kCellHasMore:@(self.sicknessHasMore), kCellHasResult:@(self.sicknessHasResult), kCellClass:@"WMSearchResultQACell", kCellTitleMoreSelector:@"relativeSicknessMoreClick", kCellDataArray:self.resultSicknessArray,kCellMoreText:@"檢視更多疾病"};
    NSDictionary *newsSectionDic     = @{kCellTitle:@"相關資訊", kCellHasMore:@(self.newsHasMore), kCellHasResult:@(self.newsHasResult), kCellClass:@"WMSearchResultNewsCell", kCellTitleMoreSelector:@"relativeNewsMoreClick", kCellDataArray:self.resultNewsArrray, kCellMoreText:@"檢視更多資訊"};
    NSDictionary *recommentMallSectionDic        = @{kCellTitle:@"熱門商品", kCellHasMore:@(self.mallHasMore), kCellHasResult:@(self.mallHasResult), kCellClass:@"WMSearchResultMallCell", kCellTitleMoreSelector:@"relativeMallMoreClick", kCellDataArray:self.resultMallArrray, kCellMoreText:@"檢視更多商品"};
    NSDictionary *recommentDoctorSectionDic      = @{kCellTitle:@"周邊熱門醫生", kCellHasMore:@(self.doctorHasMore), kCellHasResult:@(self.doctorHasResult), kCellClass:@"WMSearchResultDoctorCell", kCellTitleMoreSelector:@"recomendDoctorMoreClick", kCellDataArray:self.resultDoctorArrray, kCellMoreText:@"檢視更多醫生"};
    NSDictionary *recommentHospitalSectionDic    = @{kCellTitle:@"周邊熱門醫院", kCellHasMore:@(self.hospitalHasMore), kCellHasResult:@(self.hospitalHasResult), kCellClass:@"WMSearchResultHospitalCell", kCellTitleMoreSelector:@"recomendHospitalMoreClick", kCellDataArray:self.resultHospitalArrray, kCellMoreText:@"檢視更多醫院"};
    NSDictionary *recommentQaSectionDic          = @{kCellTitle:@"熱門問答", kCellHasMore:@(self.QAHasMore), kCellHasResult:@(self.QAHasResult), kCellClass:@"WMSearchResultQACell", kCellTitleMoreSelector:@"recomendQAMoreClick", kCellDataArray:self.resultQAArrray, kCellMoreText:@"檢視更多問答"};
    NSDictionary *recommentNewsSectionDic        = @{kCellTitle:@"熱門資訊", kCellHasMore:@(self.newsHasMore), kCellHasResult:@(self.newsHasResult), kCellClass:@"WMSearchResultNewsCell", kCellTitleMoreSelector:@"recomendNewsMoreClick", kCellDataArray:self.resultNewsArrray, kCellMoreText:@"檢視更多資訊"};
    self.impSearchResultDataArray = @[mallSectionDic, doctorSectionDic, hospitalSectionDic, qaSectionDic, newsSectionDic, recommentMallSectionDic, recommentDoctorSectionDic, recommentHospitalSectionDic, recommentQaSectionDic, recommentNewsSectionDic];
複製程式碼
  • 用一個陣列控制,裡面對應各自section的字典,字典裡面各自負責自己的titleSection和moreSection的顯示及點選事件/結果bool/各自的自定義cell/各自的資料來源。

  • 因為字典裡的資料來源NSMutableArray是淺拷貝 由指標指著地址,所以後面在資料請求後能正常一直使用。

  • 字典裡的cell,通過字串轉class的方法,獲取到對應的cell,這些cell都作統一的陣列賦值 再在各自的賦值方法裡用各自的model取值,用共性來解耦。

  • 字典裡的點選事件,用imp來實現。通過Runtime的訊息傳遞機制,直接執行imp指向的函式實現,這樣省去了Runtime訊息傳遞過程中所做的一系列查詢操作,會比直接向物件傳送訊息還要高效一些。

程式碼如下:

/** 搜尋結果 -- cellForRow **/
- (UITableViewCell *)searchResultTableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    // 11 section --- nodata,熱門商城,醫生,醫院,問答,資訊,相關附近商城,醫生,附近醫院,問答,資訊
    if (indexPath.section == 0) {
        // noData - Cell
        kWeakSelf
        WMSearchResultNoDataCell *cell = [tableView dequeueReusableCellWithIdentifier:[WMSearchResultNoDataCell reuseIdentifier] forIndexPath:indexPath];
        [cell configSearchText:self.searchKeyWords searchNoDataType:SearchNoDataTypeTabSummary];
        cell.moreBtnClickBlock = ^{
            [weakSelf skipToAskViewController];
        };
        return cell;
    }else {
        NSDictionary *impDictionary = self.impSearchResultDataArray[indexPath.section-1];
        if (indexPath.row == 0) {
            /*** sectionTitle - cell ***/
            WMSearchResultSectionTitleCell *cell = [tableView dequeueReusableCellWithIdentifier:[WMSearchResultSectionTitleCell reuseIdentifier] forIndexPath:indexPath];
            [cell configSectionTitle:impDictionary[kCellTitle] hasMore:impDictionary[kCellHasMore]];
            // imp方法轉換
            [cell performSelector:@selector(setSectionTitleMoreBtnClickedBlock:) withObject:^(NSIndexPath *indexPath) {
                SEL selector = NSSelectorFromString(impDictionary[kCellTitleMoreSelector]);
                if ([self respondsToSelector:selector])]) {
                   IMP imp = [self methodForSelector:selector])];
                   void (*func)(id, SEL) = (void *)imp;
                   func(self, selector));
                }
            }];
            return cell;
        }else {
            NSArray *dataArray = impDictionary[kCellDataArray];
            BOOL hasResult     = impDictionary[kCellHasResult];
            BOOL hasMore       = impDictionary[kCellHasMore];
            NSString *moreText = impDictionary[kCellMoreText];
            if (hasMore && (indexPath.row == (dataArray.count+1))) {
                /*** sectionMore - cell ***/
                WMSearchResultSectionMoreCell *cell = [tableView dequeueReusableCellWithIdentifier:[WMSearchResultSectionMoreCell reuseIdentifier] forIndexPath:indexPath];
                [cell configMoreText:moreText];
                return cell;
            }else {
               /*** 各自內容cell  *****/
                // 獲取cell類名的重用識別符號
                NSString *cellIndentifer = impDictionary[kCellClass];
                // 通過重用標識字串建立類
                Class cellClass = NSClassFromString(cellIndentifer);
                UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIndentifer];
                if (!cell) {
                    cell = [[cellClass alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIndentifer];
                }
                if (dataArray.count > (indexPath.row-1)) {
                    if ([cell respondsToSelector:@selector(configDataArray:indexItem:)]) {
                      // 執行賦值方法
                        [cell performSelector:@selector(configDataArray:indexItem:) withObject:dataArray withObject:[NSNumber numberWithInteger:(indexPath.row-1)]];
                    }
                }
                return cell;
            }
        }
    }
}
複製程式碼

結合runtime優化tableView

performSelector是在iOS中的一種方法呼叫方式,是執行時系統負責去找方法的,在編譯時候不做任何校驗。
他可以向一個物件傳遞任何訊息,而不需要在編譯的時候宣告這些方法。所以這也是runtime的一種應用方式。
所以performSelector和直接呼叫方法的區別就在與runtime。直接呼叫編譯是會自動校驗。如果方法不存在,那麼直接呼叫在編譯時候就能夠發現,編譯器會直接報錯。
但是使用performSelector的話一定是在執行時候才能發現,如果此方法不存在就會崩潰。所以如果使用performSelector時,為了程式的健壯性,會使用檢查方法respondsToSelector。

通過Runtime的訊息傳遞機制,直接執行imp指向的函式實現:

SEL selector = NSSelectorFromString(impDictionary[kCellTitleMoreSelector]);

IMP imp = [self methodForSelector:selector];

void (*func)(id, SEL) = (void *)imp;

func(self, selector);
                   
複製程式碼

SEL : 類成員方法的指標,其實只是方法編號。
IMP:一個函式指標,儲存了方法的地址。IMP是”implementation”的縮寫,它是objetive-C 方法(method)實現程式碼塊的地址。

IMP和SEL關係:
每一個繼承於NSObject的類都能自動獲得runtime的支援。
在這樣的一個類中,有一個isa指標,指向該類定義的資料結構體,這個結構體是由編譯器編譯時為類(需繼承於NSObject)建立的。
在這個結構體中有包括了指向其父類類定義的指標以及 Dispatch table。
Dispatch table是一張SEL和IMP的對應表。
也就是說方法編號SEL最後還是要通過Dispatch table表尋找到對應的IMP,IMP就是一個函式指標,然後執行這個方法。

實現步驟:

  1. 通過方法獲得SEL 方法編號:
SEL methodId=@selector(methodName);或者SEL methodId = NSSelectorFromString(methodName); 
複製程式碼
  1. 通過方法編號獲得IMP:
IMP imp = [self methodForSelector:methodId];  
複製程式碼
  1. 執行IMP:
void (*func)(id, SEL, id) = (void *)imp;   
func(self, methodName,param);
複製程式碼

然後直接在類裡寫methodName對應的點選事件方法就可以了,不用在cellForRow裡通過代理或block寫事件或在didSeclectRow裡再根據index來判斷了。

到這cellForRow裡面的內容就差不多寫完了,再說說tableViewCell高度計算

簡單說說tableView自定義Cell的寫法,個人不建議用Xib,建議手程式碼用Masonry佈局。

masonry主要用三種寫法,make/remake/update,根據具體樣式的變化程度決定用哪個,一般make就足夠了。

程式碼規範: 匯入標頭檔案、命名、UILoad、DataLoad、PrivateAction、Delegate、LazyLoad。。

相關文章