
尽管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 stepsdispatch_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一起交流。
面试题资料或者相关学习资料都在群文件中 进群即可下载!

