写点什么

iOS 开发 -Objective-C 中的 MVVM 模式介绍

用户头像
iOSer
关注
发布于: 2021 年 06 月 08 日

尽管 iOS 生态系统从 Objective-C 每天都在进一步发展,但一些公司仍然严重依赖它。距离 WWDC 2020 的另一波创新浪潮还有一周的时间,我认为从 MVVM 模式实现开始回到 Objective-C 会很有趣。


快速提醒一下,MVVM 模式是一种架构设计模式,将逻辑分为三个主要部分:模型 - 视图 - 视图模型。如果您正在 Swift 中寻找类似的内容,我过去曾在SwiftRxSwift 中介绍过这个主题。


让我们深入代码

模型

对于这个示例应用程序,我正在构建一个播放列表应用程序,列出歌曲名称、艺术家姓名和专辑封面。


// 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来获取一份详细的大厂面试资料为你的跳槽多添一份保障。


用户头像

iOSer

关注

微信搜索添加微信 mayday1739 进微信群哦 2020.09.12 加入

更多大厂面试资料进企鹅群931542608

评论

发布
暂无评论
iOS开发-Objective-C 中的 MVVM 模式介绍