尽管 iOS 生态系统从 Objective-C 每天都在进一步发展,但一些公司仍然严重依赖它。距离 WWDC 2020 的另一波创新浪潮还有一周的时间,我认为从 MVVM 模式实现开始回到 Objective-C 会很有趣。
快速提醒一下,MVVM 模式是一种架构设计模式,将逻辑分为三个主要部分:模型 - 视图 - 视图模型。如果您正在 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
从源获取数据并将其链接到定义的解析器以获得最终结果。
实现将依赖于模拟的 JSON 文件,因为我还没有任何 API。
// 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
复制代码
转到 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>
用来避免暴露特定类型的对象,以防将来需要新的实现。
最后,我们可以使用我们的 fetcher 并将任何结果Song
转换为SongDisplay
并始终以完成完成。fetcher 负责从正确的位置获取数据,并将其格式化回正确的模型。
我们已经准备好用 View 本身来完成它。
看法
为了表示视图,我使用 aUIViewController
并且因为它是一个相当小的应用程序,所以我也将在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
获取数据并确保在主线程上相应地重新加载数据。我们可以进一步推进并使用 diffable 算法仅更新必要的内容,而不是重新加载所有内容。
这些单元是基于SongDisplay
模型构建的,因此不会暴露于任何业务逻辑,UI 始终与其余部分保持分离。
UI 的其余部分直接通过 Storyboard 实现,以加快设计速度。
最后,我们有一个完整的 MVVM 模式实现**,分层清晰:代码易于维护和测试**。
像每个解决方案一样,没有灵丹妙药,总是需要权衡取舍。如上所述,使用闭包而不是委托是我的选择,但如果您觉得这种方法有限,您可能希望选择一种更具可读性的方法。
我没有特意涉及一些领域,比如加载图像封面、实现网络 api 或错误处理,因为它比本文的目标更远一些。
您可以在 Github 上的项目本身中找到更多详细信息,名为ObjectiveCSample。
文末推荐:iOS 热门文集
资料推荐
如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群1012951431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。
评论