上一篇我們主要從理論上講述如何通過 轉移依賴 來輕量化我們的 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+人,很快就要達到上限(抓住機會哦),關注公眾號獲取加群方式。