1. 程式人生 > >MVVM初嘗試--UITableView資料Manager思路分享

MVVM初嘗試--UITableView資料Manager思路分享

本豺狼最近忙於新需求開發, 荒於研究, 心中倍感焦慮, 不過恰好專案中進行了一些新的嘗試, 自覺收穫頗豐, 趕緊著與諸位分享!
大體說下情況吧, 豺狼這期的需求中有一塊是修改詳情頁的模組順序及UI, 由於這個詳情頁是很老的程式碼了, 十多個模組並且基於UITableView開發的, 加之迭代中不斷新增刪除模組, 可想而知UITableView代理方法多麼的混亂和不堪入目, 邏輯死板, 牽一髮動全身, 總之非常糟糕. 正好藉著這期需求, 豺狼將整個詳情頁的業務邏輯梳理並重構! 好吧, 我們進入正題:


我是封面!
重構思路是什麼

重構首先一點就是要有一個整體的思路, 對於詳情頁而言, 由於不同模組之間差異性大, 模組種類多, 順序及數量改動多

的特點, 網路上部分人選擇了UIScrollView作為底層View來開發, 這就涉及到了模組之間高度的計算, 由於豺狼沒有親身實踐, 不敢斷言合理與否, 但自覺相對麻煩, 靈活性也差一些, 自然維護起來多少會出現問題, 所以豺狼還是繼承原有邏輯, 在UITableView上進行模組的新增, 當然如果偏頗之處, 請諸位指點一二.
底層View確定後就是具體的程式碼結構邏輯了, 既然詳情頁有諸多特點, 那麼我們如何考慮結構呢? 我的思路是使用一個SectionTypeArray來進行具體模組的管理, 建立一個列舉來體現出所有模組的種類, 這樣每次需求變更時, 只要修改相應的SectionTypeArray
就可以優雅地管理不同模組.

typedef NS_ENUM(NSUInteger, ViewControllerSectionType) {
    ViewControllerSectionTypeUser,
    ViewControllerSectionTypeSport,
    ViewControllerSectionTypeFavortiteFood,
};

- (NSArray *)sectionTypeArray {
    if (_sectionTypeArray == nil) {
        _sectionTypeArray = @[@(ViewControllerSectionTypeUser),
                              @(ViewControllerSectionTypeSport),
                              @(ViewControllerSectionTypeFavortiteFood)];
    }
    return
_sectionTypeArray; }
如何管理UITableView的資料

既然我們確定了模組管理策略, 那就要在這個策略基礎上構造程式碼, 首先就是UITableView需要根據這個模組管理策略來靈活變動, 這樣的話我們代理方法中就不能把邏輯寫死, 給我啟發的是早前看過的一個Demo--SigmaTableViewModel.
我們只需要把UITableView的所有資料都用一個模型儲存了起來, 具體呼叫的時候根據NSIndexPath從陣列中取出來不就好了? return Array[IndexPath.row]多麼優雅的寫法! 於是乎問題迎刃而解.
不過我認為這個Demo中封裝的過於死板, 所以自己單獨封裝了一個簡易版的.

@interface HRTableViewCellModel : NSObject
@property (nonatomic, assign) CGFloat cellHeight;
@property (nonatomic, copy) UITableViewCell *(^configCellBlock)(NSIndexPath *indexPath, UITableView *tableView);
@property (nonatomic, copy) void (^selectCellBlock)(NSIndexPath *indexPath, UITableView *tableView);
/*
 其他屬性按需求新增
 */
@end

@interface HRTableViewModel : NSObject
@property (nonatomic, strong) NSMutableArray <HRTableViewCellModel *> *cellModelArray;
@property (nonatomic, assign) CGFloat headerHeight;
@property (nonatomic, assign) CGFloat footerHeight;
@property (nonatomic, strong) NSString *headerTitle;
@property (nonatomic, strong) NSString *footerTitle;
@property (nonatomic, strong) UIView *headerView;
@property (nonatomic, strong) UIView *footerView;
@property (nonatomic, copy) UIView * (^headerViewBlock)(NSInteger section, UITableView *tableView);
@property (nonatomic, copy) UIView * (^footerViewBlock)(NSInteger section, UITableView *tableView);
@end

所有的資料都用這個模型儲存起來, 用的時候直接讀取模型即可, 任你是順著排列倒著排列還是跳著排列隨便你, 想想就激動~
這個模型是用block來傳遞具體的業務邏輯程式碼塊, 因為是簡易版有些功能可以自己拓展, 需要提到的一點就是對於使用block造成的迴圈引用一定要注意!

靈活的詳情頁

先來展示下詳情頁有多麼靈活

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return self.sectionModelArray.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    HRTableViewModel *model = self.sectionModelArray[section];
    return model.cellModelArray.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    HRTableViewModel *model = self.sectionModelArray[indexPath.section];
    HRTableViewCellModel *cellModel = model.cellModelArray[indexPath.row];
    return cellModel.cellHeight;
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 40;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    HRTableViewModel *model = self.sectionModelArray[indexPath.section];
    HRTableViewCellModel *cellModel = model.cellModelArray[indexPath.row];
    return cellModel.configCellBlock(indexPath, tableView);
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    HRTableViewModel *model = self.sectionModelArray[indexPath.section];
    HRTableViewCellModel *cellModel = model.cellModelArray[indexPath.row];
    cellModel.selectCellBlock(indexPath, tableView);
}

就是這樣, 沒有一行具體的業務邏輯在這裡, 所有的相關業務都在外面實現好, 代理方法直接呼叫即可.

簡單說明一下

首先我們需要一個SectionModelArray來儲存UITableView的資料, 然後我們根據SectionTypeArray中的模組型別, 用switch來塞入資料.

+ (NSArray <HRTableViewCellModel *> *)reformDataToSectionModelArray:(id)data delegate:(id)delegate {
    ViewControllerDataSource *dataSource = [[ViewControllerDataSource alloc] init];
    dataSource.delegate = delegate;
    [dataSource.sectionModelArray removeAllObjects];
    dataSource.dataDic = (NSDictionary *)data;
    for (NSNumber *type in dataSource.sectionTypeArray) {
        switch (type.integerValue) {
            case ViewControllerSectionTypeUser: {
                [dataSource configUserCellModel];
            }
                break;
            case ViewControllerSectionTypeSport: {
                [dataSource configSportCellModel];
            }
                break;
            case ViewControllerSectionTypeFavortiteFood: {
                [dataSource configFavoriteFoodCellModel];
            }
                break;
            default:
                break;
        }
    }
    return [dataSource.sectionModelArray copy];
}
// 舉例: 塞入UserCell的資料
- (void)configUserCellModel {
    // section的資料
    HRTableViewModel *sectionModel = [[HRTableViewModel alloc] init];
    [sectionModel setHeaderTitle:@"使用者資訊"];
    // 塞入SectionModelArray
    [self.sectionModelArray addObject:sectionModel];
    // 具體cell的資料
    HRTableViewCellModel *cellModel = [[HRTableViewCellModel alloc] init];
    [cellModel setConfigCellBlock:^UITableViewCell *(NSIndexPath *indexPath, UITableView *tableView) {
        __weak typeof(&*self) weakSelf = self;
        UserCell *cell = (UserCell *)[weakSelf configUserCell:tableView indexPath:indexPath];
        return cell;
    }];
    [cellModel setSelectCellBlock:^(NSIndexPath *indexPath, UITableView *tableView) {
        __weak typeof(&*self) weakSelf = self;
        ViewController *vc = [[ViewController alloc] init];
        [((ViewController *)weakSelf.delegate).navigationController pushViewController:vc animated:YES];
    }];
    // 塞入section的cellModelArray中
    [sectionModel.cellModelArray addObject:cellModel];
}

至此一個模組的邏輯就新增好了, 其他模組類似的邏輯一個個新增進去.
另外對於cell的動態高度, 如果是model中計算高度, 直接寫就可以, 如果是cell中利用控制元件計算高度, 就要在configCell方法中重新從陣列中取出對應CellModel傳入, 並且實現- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath方法. 再次強調切忌迴圈引用!

關於MVVM的設計模式感想

對於看著逼格很高的MVVM, 豺狼其實瞭解的並不多, 但是借用田神Cosa Yaloyum部落格中的一張圖可以更好地說明, 部落格在此, 乾貨很多.


http://www.sprynthesis.com/2014/12/06/reactivecocoa-mvvm-introduction/


豺狼對此理解是所謂MVVM模式就是在MVC下不斷對其Controller瘦身而形成具體的處理弱業務的ViewModel, 我覺得更應該叫做"M-VM-C-V".
隨著專案不斷迭代更新, Controller中承載了很多不能在ViewModel中寫的弱業務程式碼, 造成其體積越來越大, 維護越發困難, 其中就包括UITableViewdatasourcedelegate的代理用到的邏輯, 所以豺狼嘗試著在這個Demo的基礎上分離這些弱業務邏輯, 對Controller進行瘦身!


新的結構圖

於是豺狼專門分離出一個用來管理UITableView資料的類ViewControllerDataSource, 按照我的理解, 這裡的ViewControllerDataSource就是MVVM中的ViewModel, 負責處理UITableView中涉及到的弱業務(所謂弱業務與強業務也是從田神那偷來的概念~).

//  ViewControllerDataSource.h
@interface ViewControllerDataSource : NSObject
+ (NSArray <HRTableViewModel *> *)reformDataToSectionModelArray:(id)data delegate:(id)delegate;
@end

傳入資料->加工成能用的資料模型->具體展示, Controller在裡面只起到了邏輯分發轉接的作用, 程式碼量大大減少, 這樣就可以集中處理強業務邏輯, 也許這個Demo中看的並不明顯, 但豺狼在專案中實踐下來, 節省了2000行程式碼....而且整體邏輯更加清晰, 各司其職.ViewControllerDataSource作為一個Manager就是處理UITableView相關.
豺狼認為以後的Controller可以用這種思路進一步拆分細化, 比如網路請求邏輯可以單獨建一個Manager管理, 唐巧的YTKNetwork就是這麼做的, 這樣一定程度上也可以起到解耦的作用.

最後

重劍無鋒,大巧不工。 ---- 《神鵰俠侶》

以這句話作為結束語, MVVM作為新的設計模式並不是死板固定的, 更多的還是根據需求進行使用, 簡單的頁面MVC, 複雜的頁面進行拆分, 不拘泥於MVVM的格式, 才是最正確的用法.
最後再次強調田神的高質量部落格, 關於架構的部分每一篇都能讀一天~
如果諸位能看到這裡,希望可以給豺狼點個贊鼓勵一下!



文/赤脊山的豺狼人(簡書作者)
原文連結:http://www.jianshu.com/p/a31d2b606e94
著作權歸作者所有,轉載請聯絡作者獲得授權,並標註“簡書作者”。