MVVM 與 tableView,避免膠水代碼

TL.DR

這是對前文基於 message forwarding 的輕量依賴注入容器實現 的一個實踐。git

利用一個封裝了 VC 與 VM 的類實例,經過 message forwarding 將 tableView.dataSource 和 tableView.delegate 轉發給 VC 或 VM,從而避免這樣的代碼github

// VC.m
#pragma mark - UITableViewDataSource && UITableViewDelegate
// ...

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [self.viewModel tableView:tableView numberOfRowsInSection:section];
}
複製代碼

MVVM 與 tableView

考慮到 tableView.dataSource 和 tableView.delegate 多多少少涉及到 view,一般會這麼作swift

- (void)viewDidLoad
{
    // ...
    // VC 做爲 tableView.dataSource 和 tableView.delegate 
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    // ...
}

// 再經過膠水代碼調用 VM 的對應實現
#pragma mark - UITableViewDataSource && UITableViewDelegate
// ...

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [self.viewModel tableView:tableView numberOfRowsInSection:section];
}

// ...
複製代碼

抹一點點膠水沒問題,但 app 裏大部分頁面都是 tableView 時,抹起來就很煩了。app

理想中的效果

VC 負責實現 view 相關的方法,VM 負責實現數據相關的方法,兩者由 proxy 封裝起來。post

tableView 的每一次方法調用都經過 proxy,傳遞給 VC 或 VM 中實現了該方法的一方。單元測試

// vc.m
- (void)viewDidLoad
{
    // ...
    self.proxy = [[XXXTableViewProxy alloc] initWithVC:self vm:self.viewModel];
    self.tableView.dataSource = proxy;
    self.tableView.delegate = proxy;
    // ...
}

#pragma mark - UITableViewDataSource && UITableViewDelegate
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[XXXCell reuseID]
                                                            forIndexPath:indexPath];
    id model = [self.viewModel modelForRowAtIndexPath:indexPath];
    [cell configWithModel:model];
    
    return cell;
}

// vm.m
#pragma mark - UITableViewDataSource && UITableViewDelegate
- (NSInteger)tableView:(UITableView *)_ numberOfRowsInSection:(NSInteger)section
{
    return self.dataSource.count;
}
複製代碼

簡直是天堂。測試

造天堂咯

有了 NSFPrioritizedDelegate,一切都很簡單。spa

// NSFTableViewDelegateProxy.h
/**
 將 UITableView.dataSource/delegate 的方法調用轉發給傳入的 viewModel 或 VC
 */
@interface NSFTableViewDelegateProxy : NSFPrioritizedDelegate<UITableViewDataSource, UITableViewDelegate>

- (instancetype)initWithViewController:(UIViewController<NSFAllOptionalTableViewDataSource, UITableViewDelegate> *)viewController
                             viewModel:(id<NSFAllOptionalTableViewDataSource, UITableViewDelegate>)viewModel NS_DESIGNATED_INITIALIZER;

- (instancetype)initWithDelegates:(NSArray<id<NSObject>> *)delegates
                          weakRef:(BOOL)weakRef NS_UNAVAILABLE;

@end

// NSFTableViewDelegateProxy.m
// ...
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wprotocol"
@implementation NSFTableViewDelegateProxy
#pragma clang diagnostic pop

- (instancetype)initWithViewController:(UIViewController<NSFAllOptionalTableViewDataSource, UITableViewDelegate> *)viewController
                             viewModel:(id<NSFAllOptionalTableViewDataSource, UITableViewDelegate>)viewModel
{
    if (self = [super initWithDelegates:@[viewController, viewModel] weakRef:YES])
    {
        _viewController = viewController;
        _viewModel = viewModel;
        
        self.cellForRowAtIndexPath = @selector(tableView:cellForRowAtIndexPath:);
        self.didSelectRowAtIndexPath = @selector(tableView:didSelectRowAtIndexPath:);
    }
    
    return self;
}

#pragma mark - Rule
- (id<NSObject>)delegateRules:(SEL)selector
{
    if (selector == self.cellForRowAtIndexPath)
    {
        return self.viewController;
    }
    else if (selector == self.didSelectRowAtIndexPath)
    {
        if ([self.viewController respondsToSelector:selector])
        {
            return self.viewController;
        }
    }
    else if ([self.viewController respondsToSelector:selector])
    {
        return self.viewController;
    }
    else if ([self.viewModel respondsToSelector:selector])
    {
        return self.viewModel;
    }
    
    return nil;
}
複製代碼

delegateRules 中,code

  1. cellForRowAtIndexPath 和 didSelectRowAtIndexPath 強制指定轉發給 VC
  2. VC 優先於 VM 斷定,即兩者都實現某方法,VC 勝出

用起來是這樣的get

- (void)viewDidLoad
{
    // ...
    self.proxy = [[NSFTableViewDelegateProxy alloc] initWithViewController:self viewModel:self.viewModel];
    self.tableView.dataSource = self.proxy;
    self.tableView.delegate = self.proxy;
    // ...
}
複製代碼

然後在 VC 和 VM 中各自實現須要的方法便可 😂😂。

代碼

完整實現見 NSFTableViewDelegateProxy,對應的單元測試見 NSFTableViewDelegateProxySpec

相關文章
相關標籤/搜索