上一篇我们主要从理论上讲述如何通过 转移依赖 来轻量化我们的 ViewController
,同时在 View
层和 Store
层之间传输数据。在这一篇中,我们将通过 Demo
来更清晰地描述 Minya
框架的实际操作,包括如何去构造 pipeline
,如何去构建三层对象对 pipeline
的依赖,以及数据如何通信。
Demo
的基本效果可查看公众号(这里无法上传视频)。
这里主要展示了两个页面,其对应的 ViewController
分别是 SearchViewController
和 InterestingnessViewController
。后面的示例主要以这两个类为主。
由于接口是基于 Frickr
的,所以如果想运行起来,需要去 Frickr
上注册一个应用,以获取 API KEY
和密钥,并填充到 AppDelegate.m
的下面一行代码中。(另外还请自备梯子)
[[FlickrKit sharedFlickrKit] initializeWithAPIKey:@"This is your API key" sharedSecret:@"This is the secret"];
复制代码
Scene
在进入主题之前,先来了解一下 Scene
对象。我们将一个页面描述为一个 Scene
对象,其代码如下:
@interface MIScene : NSObject
@property (nonatomic, copy, nonnull) NSString *viewName; //!< view name
@property (nonatomic, copy, nonnull) NSString *controllerName; //!< controller name
@property (nonatomic, copy, nonnull) NSString *storeName; //!< store name
+ (instancetype _Nullable)sceneWithView:(NSString * _Nonnull)viewName controller:(NSString * _Nonnull)controllerName store:(NSString * _Nonnull)storeName;
@end
复制代码
这个类很简单,只是用来组合一个页面的三层对象。这个类并不是必须的,只是为了表达清晰。注意这里三个属性的类型都是 NSString
,意味着我们将通过反射机制来创建一个 ViewController
对象及其关联的 Store
和 View
(同时也意味了更多的硬编码,我们会在后面说明)。
具体的创建过程在 MIMediator
中,代码如下所示:
- (UIViewController *)viewControllerWithScene:(MIScene *)scene context:(NSDictionary<NSString *,id> *)context callback:(MICallback)callback {
Class controllerClass = NSClassFromString(scene.controllerName);
Class storeClass = NSClassFromString(scene.storeName);
Class viewClass = NSClassFromString(scene.viewName);
id<MIStore> store = [[storeClass alloc] initWithContext:context];
return [[controllerClass alloc] initWithStore:store viewClass:viewClass callback:callback];
}
复制代码
MIMediator
是页面跳转的一个中介者,主要是负责横向数据流操作,在这不多解释。
因此,创建及使用一个 Scene
看起来是下面这样:
MIScene *scene = [MIScene sceneWithView:@"SearchView" controller:@"SearchViewController" store:@"SearchStore"];
UIViewController *viewController = [[MIMediator sharedMediator] viewControllerWithScene:scene context:nil];
UINavigationController *navigation = [[UINavigationController alloc] initWithRootViewController:viewController];
复制代码
正如上一篇后面所说,ViewController
对 View
层和 Store
层的了解仅限于一两个接口,而三层同时依赖于同一个 pipeline
,这就意味着如果两个 View
都依赖于同一个 pipeline
,那么这两个 View
可以相互替换,同理 ViewController
和 Store
也一样。这样,我们就可以根据 pipeline
来拼装三层对象。即如果 View
调整了,但整体展示的数据还是那些,那我们的 ViewController
和 Store
都不需要变动,在创建 Scene
时,我们换一个 View
名就行了。(当然这是一种理想状态)。
构造 pipeline
由于 View
、ViewController
和 Store
都是依赖于 pipeline
,所以 pipeline
可以说是整个框架的核心。如何去构造 pipeline
,将决定整个结构的灵活性。可以从两个角度来考虑这个问题:
- 根据
View
层来构造pipeline
对于一个 App
来说,View
层是和用户直接交互的,它即是用户输入数据的来源,也是业务数据的表述。采集和显示哪些数据,都需要根据 View
来确定。另外,一个复杂的 View
层可能由多个甚至多层子 View
构成,每个子 View
有不同的数据需求,所以它能更精细地去表述数据,包括页面点击这种 flag
数据。因此,我们可以根据视图的树形结构,构造一棵类似的 pipeline
树形结构。如下图所示:
这种方案还有一个好处,每一个子视图只需要依赖于其对应的 pipeline
即可,而不需要依赖于整个 pipeline
。
不过这种方案的问题在于,一旦 View
变更,将直接影响到 pipeline
的构造,进而可能影响 ViewController
及 Store
对 pipeline
属性的监听。
- 根据业务来构造
pipeline
这种方案是把业务相关的一组数据整合在一个 pipeline
对象里面(一个业务可能对应多个 View
),再把一个页面里面的多个 pipeline
组织成一棵 pipeline
树。
这种方式的优点是 pipeline
相对独立于 View
层,除了一些 View
相关的数据外,pipeline
不会受 View
过多的影响。缺点是这种 pipeline
对数据的表述比较粗旷,View
层可以监听到一些与其无关的数据。
在实际开发过程中,可以根据实际情况来确定使用哪种方案构造 pipeline
。构造 pipeline
的主要任务在 store
层中完成,因为这里是数据的处理中心。以 InterestingnessStore
为例,我们将 InterestingnessStore
的业务逻辑拆分到两个 store
中,每个 store
维护其自身的 pipeline
,然后在 InterestingnessStore
中构建起 pipeline
的层级结构。
InterestingnessPipeline.h
@interface InterestingnessPipeline : MIPipeline
@property (nonatomic, strong) TopImagePipeline *imagePipeline;
@property (nonatomic, strong) PhotoListPipeline *photoListPipeline;
- (void)setShowImageAtIndex:(NSUInteger)index;
@end
复制代码
InterestingnessStore.m
@interface InterestingnessStore ()
@property (nonatomic, strong) InterestingnessPipeline *interestPipeline; // Pipeline
@property (nonatomic, strong) TopImageStore *imageStore; // Top image store
@property (nonatomic, strong) PhotoListStore *photoStore; // Photo List store
@end
@implementation InterestingnessStore
// ...
- (InterestingnessPipeline *)interestPipeline {
if (!_interestPipeline) {
_interestPipeline = [[InterestingnessPipeline alloc] init];
_interestPipeline.imagePipeline = self.imageStore.imagePipeline;
_interestPipeline.photoListPipeline = self.photoStore.photoListPipeline;
}
return _interestPipeline;
}
@end
复制代码
以上
pipeline
层级结构与View
的层级结构对应,即我们采用方案一来构造pipeline
。
建立依赖
在 store
层中根据需求构造好 pipeline
后,就需要建立 ViewController
和 View
层对 pipeline
的依赖了,这个过程并不复杂,但是比较繁琐。这一操作主要是通过 ViewController
向上分发 pipeline
来完成的。我们再来看看 MIViewController
的实现。
@implementation MIViewController
// ...
- (void)viewDidLoad {
[super viewDidLoad];
// Set up pipeline
[self setupPipeline:self.store.pipeline];
[self.view setupPipeline:self.store.pipeline];
// Add observers of the pipeline data.
[self addObservers];
}
// ...
@end
复制代码
在 - viewDidLoad
方法里面调用了两个 - setupPipeline
:
- 第一个是
ViewController
自身的方法,这可以构建ViewController
对pipeline
的依赖,当然如果ViewController
对pipeline
没有数据需求,则子类可以不实现这个方法; - 第二个是
ViewController
的根视图的设置方法,这个方法将pipeline
传递给View
层,View
层可以根据实际需要再去设置自身的pipeline
,以及将子pipeline
分发各子View
,例如InterestingnessView
的实现:
@interface InterestingnessView ()
@property (nonatomic, strong) InterestingnessPipeline *pipeline;
@property (nonatomic, strong) TopImageView *topImageView;
@property (nonatomic, strong) PhotoListView *photoListView;
@end
#pragma mark - InterestingnessView implementation
@implementation InterestingnessView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self addSubview:self.topImageView];
[self addSubview:self.photoListView];
// ...
}
return self;
}
- (void)setupPipeline:(__kindof MIPipeline *)pipeline {
self.pipeline = pipeline;
// 分发二级 pipeline
[self.topImageView setupPipeline:self.pipeline.imagePipeline];
[self.photoListView setupPipeline:self.pipeline.photoListPipeline];
}
// ...
@end
复制代码
至此,ViewController
及 View
对 pipeline
的依赖关系构建完成。构建完成后,各层就只需要与 pipeline
打交道了,从 pipeline
中读取数据,或者把数据写入 pipeline
中。
数据传输
最后来看看最主要的部分:数据传输。回到最上面的 Demo
,我们在第二个页面中点击列表中的一个单元格,顶部的图片信息就跟着变化。我们来看看这种变化时如何产生的。我们先从目标视图即 TopImageView
说起,这个 View
监听了 TopImagePipeline
的 url
属性:
// Observe `url` property of the pipeline
[MIObserve(self.pipeline, url) changed:^(id _Nonnull newValue) {
@strongify(self)
[self mi_updateView];
}];
复制代码
也就是说,只要这个 url
发生了改变,TopImageView
就能监听到这种改变并相应地去更新视图,而至于是谁触发了这种变更,TopImageView
并不关心,同时 TopImageView
并没有提供额外的接口让父视图或者 ViewController
来调用。
这里有一点需要行说明一下,
Flickr
的flickr.photos.getRecent
接口(获取列表)并没有返回图片的url
,所以我们需要通过图片的photoID
,再去调用一次flickr.photos.getSizes
接口,以获取图片url
,于是便有了这边繁琐的流程。正好也根据这个示例来说明问题。
我们再来看看事件的发起源 photoListView
,在 PhotoListView
中,选中 table view
的操作只有一行代码:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
self.pipeline.inputSelectedPhotoIndex = indexPath.row;
}
复制代码
然后,这里 PhotoListView
只是简单的设置了它的 pipeline
的 inputSelectedPhotoIndex
属性,然后就没有任何操作了。也就是说 PhotoListView
仅仅重新设置了所选的行索引,后面如何处理便和 PhotoListView
没有关系了。
额,然后好像就断线了。两者是怎么连接起来的呢?我们来看看谁监听了 inputSelectedPhotoIndex
属性。全局搜索,我们可以看到 InterestingnessStore
对象监听了 inputSelectedPhotoIndex
属性:
[MIObserve(self.interestPipeline.photoListPipeline, inputSelectedPhotoIndex) changed:^(NSNumber * _Nonnull newValue) {
@strongify(self)
[self.interestPipeline setShowImageAtIndex:[newValue integerValue]];
}];
复制代码
在 InterestingnessPipeline
的 setShowImageAtIndex
方法(这个方法只做简单的处理)中,重新设置了其子 imagePipeline
的 photoID
属性。
然后 TopImageStore
监听了 imagePipeline
的 photoID
属性,
[MIObserve(self.imagePipeline, photoID) changed:^(NSString * _Nonnull newValue) {
@strongify(self)
[self fetchData];
}];
复制代码
并依此发起网络请求,最终获得图片的 url
,
- (void)fetchData {
// ...
@weakify(self)
[self.getSizesService requestWithParameters:@{@"photo_id": self.imagePipeline.photoID} success:^(NSString * _Nullable data) {
@strongify(self)
self.imagePipeline.url = data;
} fail:^(id _Nullable data, NSError * _Nullable error) {
// You can do something if the request failed.
}];
}
复制代码
在此更新 imagePipeline
的 url
后,TopImageView
监听到这一变更,并从 imagePipeline
中获取到 url
后,去加载图片。至此整个流程打通。我们画册个简单的图来梳理一下流程。
整个过程中,每个对象只关心自己需要监听 pipeline
中哪些属性并做出相应的操作,以及执行某些操作后去修改 pipeline
对应的属性,而不必去理会其它事情,专心做好自己的事实就好。不过这种特性也带来一些不好的后果,你可能已经发现,在后一篇中也会讲到这个问题。
小结
在这一篇中,我们结合 Demo
实践了如何去构造 pipeline
,建立三层对象对 pipeline
的依赖,以及建立依赖后,如何实现各层之间的数据通信。实际上整个流程并不复杂,只是第一次看可能有些地方不太好理解。
由于这套框架并没有太多的实战,所以更多的是一个实验性的不成熟框架。其中还有一些细节,由于篇幅限制,在此不多做介绍,有兴趣可以看看源码,一起探讨。另外我自己后来总结了一下,还存在很多问题,这些问题我会在下一篇中说明。
知识小集公众号
知识小集是一个团队公众号,每周都会有原创文章分享,我们的文章都会在公众号首发。知识小集微信群,短短几周时间,目前群友已经300+人,很快就要达到上限(抓住机会哦),关注公众号获取加群方式。