尽管iOS生态系统从Objective-C每天都在增长,但一些公司仍然严重依赖它。距WWDC 2020的另一波创新浪潮已经一周了,我认为从MVVM模式实施开始重新回到Objective-C将会很有趣。
快速提醒一下,MVVM模式是架构设计模式的三个主要解耦逻辑:模型-视图-ViewModel。如果您正在Swift中寻找类似的内容,那么我过去在Swift中也讨论了这个主题,并且在RxSwift中也进行了介绍。
让我们深入研究代码
模型
对于此示例应用程序,我正在构建一个播放列表应用程序,列出歌曲标题,艺术家姓名和专辑封面。
// Song.h
@interface Song : NSObject
@property (nonatomic, strong) NSString * title;
@property (nonatomic, strong) NSString * artistName;
@property (nonatomic, strong) NSString * albumCover;
- (instancetype)initWithTitle:(NSString*)title artistName:(NSString*)artistName albumCover:(NSString*)albumCover;
- (nullable NSURL*)albumCoverUrl;
@end
// Song.m
@implementation Song
- (instancetype)initWithTitle:(NSString*)title artistName:(NSString*)artistName albumCover:(NSString*)albumCover
{
self = [super init];
if (self) {
self.title = title;
self.artistName = artistName;
self.albumCover = albumCover;
}
return self;
}
- (nullable NSURL*)albumCoverUrl {
return [NSURL URLWithString:self.albumCover];
}
从那里开始,我想在每一层之间保持清晰的分隔,所以我使用面向协议的编程方法来使代码可维护和可测试。
一种组件是获取数据,而另一种组件将解析它们。
由于我们Result
在Objective-C中没有类型,所以我想分别将成功和错误过程解耦。为此,我对回调使用了两个闭包。即使它可以使语法的可读性降低,但我还是首选委托模式。
@protocol SongParserProtocol <NSObject>
- (void)parseSongs:(NSData *)data withSuccess:(void (^)(NSArray<Song *> *songs))successCompletion error:(void (^)(NSError *error))errorCompletion;
@end
@protocol SongFetcherProtocol <NSObject>
- (void)fetchSongsWithSuccess:(void (^)(NSArray<Song *> *songs))successCompletion error:(void (^)(NSError *error))errorCompletion;
@end
在这里,第一个协议SongParserProtocol
负责将原始数据反序列化为模型。第二种协议SongFetcherProtocol
从源获取数据并将其链接到定义的解析器以获得最终结果。
由于我还没有任何API,因此实现将依赖于模拟的JSON文件。
// SongFetcher.h
@interface SongFetcher : NSObject<SongFetcherProtocol>
- (instancetype)initWithParser:(id<SongParserProtocol>)parser;
@end
// SongFetcher.m
@interface SongFetcher()
@property (nonatomic, strong) id<SongParserProtocol> parser;
@end
@implementation SongFetcher
- (instancetype)initWithParser:(id<SongParserProtocol>)parser
{
self = [super init];
if (self) {
self.parser = parser;
}
return self;
}
/// Mocked data based on JSON file
- (void)fetchSongsWithSuccess:(void (^)(NSArray<Song *> *))successCompletion error:(void (^)(NSError *))errorCompletion {
__weak SongFetcher * weakSelf = self;
void (^dataResponse)(NSData *) = ^(NSData *data){
[weakSelf.parser parseSongs:data withSuccess:successCompletion error:errorCompletion];
};
// TODO: improve error handling at each steps
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
FileReader * reader = [[FileReader alloc] init];
[reader readJson:@"songs" withSuccess:dataResponse error:errorCompletion];
});
}
服务已准备就绪,可以在后台队列中分派工作,我们已经准备好构建ViewModel。
视图模型
由于目标是显示的表示形式Song
,因此我想创建一个特定的模型来表示其在单元格中的显示。它避免了将我们的业务模型暴露给UI组件本身。它使每个属性的显示更加明确。
// SongDisplay.h
@class Song;
@interface SongDisplay : NSObject
@property (nonatomic, readonly, nullable) NSString *title;
@property (nonatomic, readonly, nullable) NSString *subtitle;
@property (nonatomic, readonly, nullable) UIImage *coverImage;
- (instancetype)initWithSong:(nonnull Song*)song;
@end
面试题持续整理更新中,如果你正在面试或者想一起进阶,不妨添加一下交流群1012951431一起交流。
转到ViewModel,我想展示一种获取此新模型的方法。它还必须包含其他方法,以稍后提供UITableViewDataSource。
// ViewModel.h
@interface ViewModel : NSObject
- (void)getSongsWithSuccess:(void (^)(NSArray<SongDisplay*> *songs))successCompletion error:(void (^)(NSError *error))errorCompletion;
- (NSUInteger)numberOfItems;
- (NSUInteger)numberOfSections;
- (nullable SongDisplay *)itemAtIndexPath:(NSIndexPath *)indexPath;
@end
最后,我们可以实现这些方法并填补空白。重要的是将先前准备的每个协议重用到构造函数中。我可以从用于单元测试的自定义构造函数中注入它们,但暂时将其简化。
// ViewModel.m
@interface ViewModel()
@property (nonatomic, strong) id<SongFetcherProtocol> fetcher;
@property (nonatomic, strong) NSArray<SongDisplay *> *items;
@end
@implementation ViewModel
- (instancetype)init
{
self = [super init];
if (self) {
self.items = @[];
self.fetcher = [[SongFetcher alloc] initWithParser:[[SongParser alloc] init]];
}
return self;
}
- (void)getSongsWithSuccess:(void (^)(NSArray<SongDisplay *> * _Nonnull))successCompletion error:(void (^)(NSError * _Nonnull))errorCompletion {
__weak ViewModel *weakSelf = self;
[self.fetcher fetchSongsWithSuccess:^(NSArray<Song *> *songs) {
NSMutableArray * items = [[NSMutableArray alloc] init];
for (Song *song in songs) {
[items addObject:[[SongDisplay alloc] initWithSong:song]];
}
[weakSelf setItems:items];
successCompletion(items);
} error:errorCompletion];
}
- (NSUInteger)numberOfItems {
return self.items.count;
}
- (NSUInteger)numberOfSections {
return 1;
}
- (SongDisplay *)itemAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row >= self.items.count) {
return nil;
}
return self.items[indexPath.row];
}
@end
请注意,id<SongFetcherProtocol>
在将来需要新实现的情况下,我会避免公开特定类型的对象。
最后,我们可以使用提取程序并将所有结果Song
转换为,SongDisplay
并始终以完成来结束。提取程序会尽力从正确的位置获取数据,然后格式化回正确的模型。
我们准备用View本身完成它。
视图
为了表示视图,我使用,UIViewController
并且由于它是一个很小的应用程序,因此我还将在其中实现必要的功能UITableViewDataSource
。
// ViewController.h
@interface ViewController : UIViewController<UITableViewDataSource>
@property (nonatomic, strong) IBOutlet UITableView * tableView;
@end
最后,我们可以通过连接在包执行ViewController
它ViewModel
。
@interface ViewController ()
@property (nonatomic, strong) ViewModel * viewModel;
@end
@implementation ViewController
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
self.viewModel = [[ViewModel alloc] init];
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self.tableView setDataSource:self];
[self getData];
}
- (void)getData {
__weak ViewController *weakSelf = self;
[self.viewModel getSongsWithSuccess:^(NSArray<SongDisplay *> * _Nonnull songs) {
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.tableView reloadData];
});
} error:^(NSError * _Nonnull error) {
// TODO handle error
}];
}
//MARK: - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.viewModel.numberOfSections;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.viewModel.numberOfItems;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
SongTableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"SongTableViewCell"];
if (!cell) {
assert(false);
}
[cell setDisplay:[self.viewModel itemAtIndexPath:indexPath]];
return cell;
}
@end
在此实现中,我触发ViewModel
来获取数据并确保相应地在主线程上重新加载数据。我们可以进一步推进,并使用差异算法仅更新必要的内容,而不是重新加载所有内容。
单元是基于SongDisplay
模型构建的,因此不会暴露于任何业务逻辑,UI始终与其余部分保持分离。
UI的其余部分直接通过Storyboard实现,以加快设计速度。
最后,我们有一个完整的MVVM模式实现,其中各层之间明确分开:代码易于维护和测试。
像每个解决方案一样,没有万灵药,总会有一些折衷。如上所述,使用闭包而不是委派是我的选择,但是如果您觉得这种局限性,则可能希望选择一种更具可读性的方法。
我没有故意涵盖某些领域,例如加载图像封面,实现网络api或错误处理,因为这比本文的目标要远一些。
您可以在Github上名为ObjectiveCSample的项目中找到更多细节。
面试资料:
面试题持续整理更新中,如果你正在面试或者想一起进阶,不妨添加一下交流群1012951431一起交流。
面试题资料或者相关学习资料都在群文件中 进群即可下载!