MVVM 与 tableView,避免胶水代码

1,293 阅读2分钟

TL.DR

这是对前文基于 message forwarding 的轻量依赖注入容器实现 的一个实践。

利用一个封装了 VC 与 VM 的类实例,通过 message forwarding 将 tableView.dataSource 和 tableView.delegate 转发给 VC 或 VM,从而避免这样的代码

// 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,通常会这么做

- (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 时,抹起来就很烦了。

理想中的效果

VC 负责实现 view 相关的方法,VM 负责实现数据相关的方法,二者由 proxy 封装起来。

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,一切都很简单。

// 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 中,

  1. cellForRowAtIndexPath 和 didSelectRowAtIndexPath 强制指定转发给 VC
  2. VC 优先于 VM 判定,即二者都实现某方法,VC 胜出

用起来是这样的

- (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