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 中,
- cellForRowAtIndexPath 和 didSelectRowAtIndexPath 强制指定转发给 VC
- 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。