8個模式幫你消除iOS代碼中的巨大View Controller
摘要:隨著功能的累計,View Controller的體量會變得巨大。鍵盤管理、用戶輸入、數據變形、視圖分配——這些東西當中哪個才是真正的View Controller範圍?哪些東西應該指派給其他對象?在這篇文章中,我們將會探索將這些職責隔離進其各自對象的方式。這樣做能幫助我們簡化代碼,讓代碼獲得更高的可讀性。
在一個ViewController中,這些職責可以被統一放在#pragma區域中。但是,我們其實應該考慮將它拆分,並且放在更小的原件中。
數據源
數據源模式(Data Source Pattern)是一種用來隔離哪個對象對應哪個引導路徑的邏輯的方式。尤其是在複雜的圖標視圖中,這個模式非常實用,可以用來移除View Controller里所有「哪些cell在特定條件下可見」的邏輯。如果你曾經寫過這樣的圖標,經常需要對row和section的整數進行對比,那麼數據源模式非常適合你。
數據源模式可以和UITableViewDataSource共存,但是我發現用這些對象對cell進行配置,其發揮的作用於管理引導路徑時不太一樣,因此我比較喜歡將兩者分開。
這個簡單的數據源模式使用實例,可以幫你處理分段邏輯:
@implementation SKSectionedDataSource : NSObjectn - (instancetype)initWithObjects:(NSArray*)objects sectioningKey:(NSString *)sectioningKey n { nn self = [super init]; n if (!self) return nil;n [self sectionObjects:objectswithKey:sectioningKey]; n return self;n}nn-(void)sectionObjects:(NSArray *)objects withKey:(NSString *)sectioningKey { n self.sectionedObjects = //section theobjects arrayn}nn-(NSUInteger)numberOfSections { n return self.sectionedObjects.count;n}nn-(NSUInteger)numberOfObjectsInSection:(NSUInteger)section { n return [self.sectionedObjects[section]count];n}nn-(id)objectAtIndexPath:(NSIndexPath *)indexPath {n returnself.sectionedObjects[indexPath.section][indexPath.row];n}n@endn
標準合成(Standard Composition)
蘋果在發布iOS5的時候,一同推出了View Controller Containment API。你可以使用這個API對View Controller進行合成。如果你的ViewController由多個邏輯單元所構成,你可以考慮將其拆分。
在一個擁有header和grid視圖的屏幕上,我們可以載入兩個View Controller,然後將他們放在正確的位置上。
-(SKHeaderViewController *)headerViewController { n if (!_headerViewController) {n SKHeaderViewController*headerViewController = [[SKHeaderViewController alloc] init];n [selfaddChildViewController:headerViewController];n [headerViewControllerdidMoveToParentViewController:self];nn [self.viewaddSubview:headerViewController.view]; n self.headerViewController =headerViewController;n } n return _headerViewController;n}nn-(SKGridViewController *)gridViewController { n if (!_gridViewController) {n SKGridViewController*gridViewController = [[SKGridViewController alloc] init];nn [selfaddChildViewController:gridViewController];n [gridViewControllerdidMoveToParentViewController:self];nn [self.viewaddSubview:gridViewController.view]; n self.gridViewController =gridViewController;n } n return _gridViewController;n}nn-(void)viewDidLayoutSubviews {n [super viewDidLayoutSubviews]; n CGRect workingRect = self.view.bounds; n CGRect headerRect = CGRectZero, gridRect =CGRectZero; n CGRectDivide(workingRect, &headerRect,&gridRect, 44, CGRectMinYEdge); n self.headerViewController.view.frame = tagHeaderRect; n self.gridViewController.view.frame =hotSongsGridRect;nn}n
Smarter Views
如果你是在ViewController的類中對所有子視圖進行分配,你可以考慮使用Smarter View。UIViewController默認情況下會使用UIView來瀏覽屬性,但是你也可以用自己的視圖去取代它。你可以使用-loadView作為接入點,前提是你要在那個方法中設定了self.view。
@implementationSKProfileViewControllernn- (void)loadView { n self.view = [SKProfileView new];n}n//...@endn@implementationSKProfileView : NSObjectn- (UILabel *)nameLabel { n if (!_nameLabel) { n UILabel *nameLabel = [UILabel new]; n //configure font, color, etcn [self addSubview:nameLabel]; n self.nameLabel = nameLabel;n } n return _nameLabel;n}nn- (UIImageView*)avatarImageView { n if (!_avatarImageView) { n UIImageView * avatarImageView =[UIImageView new];n [self addSubview:avatarImageView]; n self.avatarImageView = avatarImageView;n } n return _avatarImageViewn}nn-(void)layoutSubviews { n //perform layoutn}n @endn
你也可以重新定義@property(nonatomic) SKProfileView *view,因為它是一個比UIView更具體的類別,分析器會將self.view視為 SKProfileView,從而完成正確的處理。
Presener模式
Presenter模式可以包裹模型對象,改變它的顯示屬性,並且公開那些已被改變的屬性的消息。在其他一些情境中,它也被稱為Presentation Model、Exhibit模式和ViewModel等。
@implementation SKUserPresenter : NSObjectn -(instancetype)initWithUser:(SKUser *)user { n self = [super init]; n if (!self) return nil;n _user = user; n return self;n}nn- (NSString *)name{ n return self.user.name;n}nn- (NSString *)followerCountString{ n if (self.user.followerCount == 0) { n return @"";n } n return [NSString stringWithFormat:@"%@followers", [NSNumberFormatterlocalizedStringFromNumber:@(_user.followerCount)numberStyle:NSNumberFormatterDecimalStyle]];n}nn- (NSString*)followersString { n NSMutableString *followersString =[@"Followed by " mutableCopy];n [followersStringappendString:[self.class.arrayFormatter stringFromArray:[self.user.topFollowersvalueForKey:@"name"]]; n return followersString;n}nn+(TTTArrayFormatter*) arrayFormatter { n static TTTArrayFormatter *_arrayFormatter; n static dispatch_once_t onceToken; n dispatch_once(&onceToken, ^{n _arrayFormatter = [[TTTArrayFormatteralloc] init];n _arrayFormatter.usesAbbreviatedConjunction = YES;n }); n return _arrayFormatter;n}@endn
最重要的是,模型對象本身不會被暴露。Presenter扮演了模型看門人的角色。這保證了View Controller無法繞開Presenter而直接訪問模型。
Binding模式
Binding模式在變化的過程中會使用模型數據對視圖進行更新。Cocoa非常適合使用這個模式,因為KVO能夠觀察模型,並且從模型中進行讀取,在視圖中完成寫入。Cocoa Binding是這個模式的AppKit版本。Reactive Cocoa等第三方庫也非常適合這個模式。
@implementationSKProfileBinding : NSObjectn-(instancetype)initWithView:(SKProfileView *)view presenter:(SKUserPresenter*)presenter { n self = [super init]; n if (!self) return nil;n _view = view;n _presenter = presenter; n return self;n}nn- (NSDictionary*)bindings { n return @{ @"name":@"nameLabel.text", @"followerCountString":@"followerCountLabel.text",n };n}nn- (void)updateView{n [self.bindingsenumerateKeysAndObjectsUsingBlock:^(id presenterKeyPath, id viewKeyPath, BOOL*stop) { n id newValue = [self.presentervalueForKeyPath:presenterKeyPath];n [self.view setObject:newvalueforKeyPath:viewKeyPath];n }];n}@endn
(Note that oursimple presenter from above isn』t necessarily KVO-able, but it could be made tobe so.)
Interaction模式
View Controller變得體量過大的重要原因之一,就是actionSheet.delegate= self的濫用。在Smaitalk中,Controller對象的整個角色,就是接受用戶輸入,並且更新試圖和模型。如今我們所使用的交互相對複雜,這些交互會要求我們在View Controller中寫下大量的代碼。
交互的過程通常開始與用戶的最初輸入(例如點擊按鈕)、可選的用戶再次輸入(例如「你確定要繼續嗎?」),之後程序或產生活動,例如網路請求和狀態改變。這個操作其實可以完全包裹在Interaction Object之中。
@implementationSKProfileViewControllernn- (void)followButtonTapped:(id)sender{ n self.followUserInteraction =[[SKFollowUserInteraction alloc] initWithUserToFollow:self.user delegate:self];n [self.followUserInteraction follow];n}nn-(void)interactionCompleted:(SKFollowUserInteraction *)interaction {n [self.binding updateView];n}//...@endn
@implementationSKFollowUserInteraction : NSObject <UIAlertViewDelegate>nn-(instancetype)initWithUserToFollow:userdelegate:(id<InteractionDelegate>)delegate { n self = [super init]; n if !(self) return nil;n _user = user;n _delegate = delegate; n return self;n}nn- (void)follow {n [[[UIAlertView alloc] initWithTitle:nil message:@"Are you sure you want to follow this user?"n delegate:selfn cancelButtonTitle:@"Cancel"n otherButtonTitles:@"Follow", nil] show];n}nn-(void)alertView:(UIAlertView *)alertViewclickedButtonAtIndex:(NSInteger)buttonIndex { n if ([alertView buttonTitleAtIndex:buttonIndex]isEqual:@"Follow"]) {n [self.user.APIGatewayfollowWithCompletionBlock:^{n [self.delegateinteractionCompleted:self];n }];n }n}@endn
Keyboard Manager
當鍵盤狀態出現改變,視圖的更新也會在View Controller中出現卡頓,但是使用KeyboardManager模式可以很好的解決這個問題。
@implementationSKNewPostKeyboardManager : NSObjectn -(instancetype)initWithTableView:(UITableView *)tableView { n self = [super init]; n if (!self) return nil;n _tableView = tableView; n return self;n}nn- (void)beginObservingKeyboard{n [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(keyboardDidHide:)name:UIKeyboardDidHideNotification object:nil];n [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(keyboardWillShow:)name:UIKeyboardWillShowNotification object:nil];n}nn-(void)endObservingKeyboard {n [[NSNotificationCenter defaultCenter]removeObserver:self name:UIKeyboardDidHideNotification object:nil];n [[NSNotificationCenter defaultCenter] removeObserver:selfname:UIKeyboardWillShowNotification object:nil];n}nn-(void)keyboardWillShow:(NSNotification *)note { n CGRect keyboardRect = [[note.userInfoobjectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; n UIEdgeInsets contentInsets = UIEdgeInsetsMake(self.tableView.contentInset.top,0.0f, CGRectGetHeight(keyboardRect), 0.0f); n self.tableView.contentInset =contentInsets; n self.tableView.scrollIndicatorInsets = contentInsets;n}nn-(void)keyboardDidHide:(NSNotification *)note { n UIEdgeInsets contentInset =UIEdgeInsetsMake(self.tableView.contentInset.top, 0.0f,self.oldBottomContentInset, 0.0f); n self.tableView.contentInset =contentInset; n self.tableView.scrollIndicatorInsets = contentInset;n}@endn
You can call-beginObservingKeyboard and -endObservingKeyboard from -viewDidAppear and-viewWillDisappear or wherever』s appropriate.在需要的時候,你也可以從-viewDidAppear和-viewWillDisappear中調取-beginObservingKeyboard和-endObservingKeyboard。
Navigator模式
通常情況下,視圖間的切換是通過調取to -pushViewController:animated:來實現的。隨著過渡效果越來越複雜,你可以將這個任務指定給Navigator對象來完成。尤其是在同時支持iPhone和iPad的應用中,視圖切換需要根據設備屏幕尺寸的不同而改變。
@protocolSKUserNavigator <NSObject>n-(void)navigateToFollowersForUser:(SKUser *)user;n@endn@implementationSKiPhoneUserNavigator : NSObject<SKUserNavigator>n-(instancetype)initWithNavigationController:(UINavigationController*)navigationController { nn self = [super init]; n if (!self) return nil;n _navigationController =navigationController; n return self;n}nn- (void)navigateToFollowersForUser:(SKUser*)user {n SKFollowerListViewController *followerList= [[SKFollowerListViewController alloc] initWithUser:user];n [self.navigationControllerpushViewController:followerList animated:YES];n}n@endn
@implementationSKiPadUserNavigator : NSObject<SKUserNavigator>n-(instancetype)initWithUserViewController:(SKUserViewController*)userViewController { nn self = [super init]; n if (!self) return nil;n _userViewController = userViewController; n return self;n}nn-(void)navigateToFollowersForUser:(SKUser *)user {n SKFollowerListViewController *followerList= [[SKFollowerListViewController alloc] initWithUser:user]; n self.userViewController.supplementalViewController = followerList;n}n
總結
從歷史來看,蘋果的SDK只包含最小數量的原件,但是隨著越來越多的API使用,我們經常會讓View Controller的體量變得越來越大。將ViewController的職責指定給其他方式去完成,我們可以更好的控制View Controller的體積。
Christian(譯) 閱讀原文
http://weixin.qq.com/r/VjuLk3DE1C2rrTTw925E (二維碼自動識別)
推薦閱讀:
※如何看待新聞「iPhone的快充不算快充」(還有附加問題求解答!!)?
※每周iOS精品遊戲介紹 - 第141期 8.15-8.21
※一款披著製作表情包外衣的摳圖 APP
※【求證】AppStore Warning 並非針對 RN/Weex 這類技術