前言
viewController 是 iOS 使用的基本元素之一, 是 MVC 中重要的一環, 在程式碼的編寫中, viewController 經常變得很臃腫, 這是因為我們經常讓 viewController 做一些本不應該他做的事情, 比如 viewController 的 View 的較為複雜子檢視的佈局, 屬性的修改, 一些子檢視的程式碼邏輯等等.
正文
1.去除重複的, 不必要的冗餘程式碼.
剝離 tableViewDelegate 和 tableViewDataSource
tableView 是經常使用的UI控制元件, tableViewDataSource 和 tableViewDelegate 的程式碼是幾乎每個控制器裡面都會出現的東西, 那麼如果要構建的 tableView 比較簡單, 就可以考慮用通用的 tableViewDelegate 和 tableViewDataSource. 根據我個人習慣, 我一般都會讓所有 cell 的 model 繼承自一個基礎的 model
JVSBaseModel.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface JVSBaseModel : NSObject
/**
* 判斷 model 對應的 cell 是否是被選中狀態
*/
@property (nonatomic, assign) BOOL isSelected;
/**
* model 對應 cell 的 identifier
*/
@property (nonatomic, copy) NSString *identifier;
/**
* cell 的高度
*/
@property (nonatomic, assign) CGFloat cellHeight;
/**
* 子類一定要實現的方法, 一般用來根據 model 的內容來設定 identifier 以達到區分不同 cell 的目的.
*/
- (void)configureModel;
@end
複製程式碼
JVSTableViewDelegate.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@class JVSBaseModel;
@protocol JVSTabelViewSeletedDelegate
/**
* 點選 cell 的代理
@param item 點選的 cell 對應的模型
*/
- (void)selectedCellWithItem:(JVSBaseModel *)item;
@end
/**
* 通用的 tableView 代理, 支援不同樣式的 cell, 但是不支援多個 section, 也不支援 sectionHeader 和 footerHeader.
*/
@interface JVSTableViewDelegate : NSObject <UITableViewDelegate>
/**
* 點選 cell 時候處理的代理.
*/
@property (nonatomic, weak) id<JVSTabelViewSeletedDelegate, NSObject> delegate;
/**
* 初始化方法
@param items 模型陣列
@param delegate 點選cell的代理
@return 例項
*/
- (instancetype)initWithItems:(NSMutableArray *)items andSelectedDelegate:(id)delegate;
@end
複製程式碼
JVSTableViewDelegate.m
#import "JVSTableViewDelegate.h"
#import "JVSBaseModel.h"
@interface JVSTableViewDelegate()
@property (nonatomic, strong) NSMutableArray *itemsArrM;
@end
@implementation JVSTableViewDelegate
- (instancetype)initWithItems:(NSMutableArray *)items andSelectedDelegate:(id)delegate
{
if (self = [super init]) {
self.itemsArrM = items;
self.delegate = delegate;
}
return self;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
JVSBaseModel *item = self.itemsArrM[indexPath.row];
return item.cellHeight;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
JVSBaseModel *item = self.itemsArrM[indexPath.row];
if ([self.delegate respondsToSelector:@selector(selectedCellWithItem:)]) {
[self.delegate selectedCellWithItem:item];
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return 0.01;
}
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
{
return 0.01;
}
複製程式碼
JVSTableViewDataSource.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "JVSBaseModel.h"
/**
* 配置 cell 的 block
@param cell 需要配置的 cell
@param item 模型
*/
typedef void (^configureCell)(UITableViewCell *cell, JVSBaseModel *item);
/**
* 通用的簡易 tableViewDataSource, 支援多種不同的 cell, 不過不支援多個 section 以及頭部和尾部檢視
*/
@interface JVSTableViewDataSource : NSObject<UITableViewDataSource>
/**
* 配置 cell 的 block
*/
@property (nonatomic, copy) configureCell cellConfigureCellBlock;
/**
* 模型陣列
*/
@property (nonatomic, strong) NSMutableArray *itemsArrM;
/**
* 初始化方法
@param items 模型陣列
@param configureCellBlock 配置 cell 的 block
@return 例項物件
*/
- (instancetype)initWithItems:(NSMutableArray *)items andConfigureCellBlock:(configureCell)configureCellBlock;
@end
複製程式碼
JVSTableViewDataSource.m
#import "JVSTableViewDataSource.h"
#import "JVSBaseModel.h"
@implementation JVSTableViewDataSource
- (instancetype)initWithItems:(NSMutableArray *)items andConfigureCellBlock:(configureCell)configureCellBlock
{
if (self = [super init]) {
self.cellConfigureCellBlock = configureCellBlock;
self.itemsArrM = items;
}
return self;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.itemsArrM.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
JVSBaseModel *item = self.itemsArrM[indexPath.row];
if (!item.identifier) {
@throw [NSException exceptionWithName:@"identifierError" reason:
[NSString stringWithFormat:@"identifier為空"] userInfo:nil];
}
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:item.identifier];
if (nil == cell) {
@throw [NSException exceptionWithName:@"cellNilError" reason:
[NSString stringWithFormat:@"有取到對應的cell, 請檢查該tableView是否註冊了%@的cell",item.identifier] userInfo:nil];
}
self.cellConfigureCellBlock(cell, item);
return cell;
}
複製程式碼
使用方式
在 ViewController 中
_tableView = [[UITableView alloc] init];
[_tableView registerClass:[JVSTestTableViewCell class] forCellReuseIdentifier:@"JVSTestCellStyleTitle"];
[_tableView registerClass:[JVSTestTableViewCell class] forCellReuseIdentifier:@"JVSTestCellStyleImage"];
JVSTableViewDataSource *dataSource = [[JVSTableViewDataSource alloc] initWithItems:self.itemArrM andConfigureCellBlock:^(UITableViewCell *cell, JVSBaseModel *item) {
JVSTestTableViewCell *sameCell = (JVSTestTableViewCell *)cell;
JVSTestCellModel *sameItem = (JVSTestCellModel *)item;
[sameCell configureCellWithModel:sameItem];
}];
_tableView.dataSource = dataSource;
_tableViewDataSource = dataSource;
JVSTableViewDelegate *delegate = [[JVSTableViewDelegate alloc] initWithItems:self.itemArrM andSelectedDelegate:self];
_tableViewDelegate = delegate;
_tableView.delegate = delegate;
_tableView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height-200);
[self.view addSubview:_tableView];
複製程式碼
有一點需要注意, 由於 tableView 的代理是 weak 引用的, 這裡設定完代理後需要自己用一個強引用來引用新建的通用代理, 否則會發生代理自動釋放的情況, delegate 和 datasource 都是這樣.
2.不必要的邏輯, 不要讓 viewController 來實現
建立不同型別的 cell, 可以根據不同的 identifier 來實現, 但這個判斷不應該在 viewController 中實現, 而應該在 cell 中自行判斷.
JVSTestTableViewCell.h
#import <UIKit/UIKit.h>
@interface JVSTestTableViewCell : UITableViewCell
@property (nonatomic, strong) UILabel *titleL;
@property (nonatomic, strong) UIImageView *imageV;
@end
複製程式碼
這裡將兩個控制元件暴露出來是我覺得將配置 cell 的方法寫進 cell 的分類裡面去, 這樣可以有效解耦 cell 和 model.
JVSTestTableViewCell.m
#import "JVSTestTableViewCell.h"
@implementation JVSTestTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
if ([reuseIdentifier isEqualToString:@"JVSTestCellStyleImage"])
{
[self initImageCell];
} else if([reuseIdentifier isEqualToString:@"JVSTestCellStyleTitle"])
{
[self initTitleCell];
}
}
return self;
}
- (void)initTitleCell
{
_titleL = [[UILabel alloc] init];
[self.contentView addSubview:_titleL];
}
- (void)initImageCell
{
_imageV = [[UIImageView alloc] init];
[self.contentView addSubview:_imageV];
}
- (void)layoutSubviews
{
if ([self.reuseIdentifier isEqualToString:@"JVSTestCellStyleImage"]) {
self.imageV.frame = self.contentView.bounds;
} else if ([self.reuseIdentifier isEqualToString:@"JVSTestCellStyleTitle"]) {
self.titleL.frame = self.contentView.bounds;
}
}
複製程式碼
配置 cell 的分類
JVSTestTableViewCell.h
#import "JVSTestTableViewCell.h"
@class JVSTestCellModel;
@interface JVSTestTableViewCell (ConfigureCellMethod)
- (void)configureCellWithModel:(JVSTestCellModel *)item;
@end
複製程式碼
JVSTestTableViewCell.m
#import "JVSTestTableViewCell+ConfigureCellMethod.h"
#import "JVSTestCellModel.h"
@implementation JVSTestTableViewCell (ConfigureCellMethod)
- (void)configureCellWithModel:(JVSTestCellModel *)item
{
if (item.imageName.length>0) {
self.imageV.image = [UIImage imageNamed:item.imageName];
} else {
self.titleL.text = item.title;
}
}
@end
複製程式碼
3.合理使用 ViewController 的容器
在 iOS5 之前, view controller 容器是 Apple 的特權。實際上,在 view controller 程式設計指南中還有一段申明,指出你不應該使用它們。Apple 對 view controllers 的總的建議曾經是“一個 view controller 管理一個全螢幕的內容”。這個建議後來被改為“一個 view controller 管理一個自包含的內容單元”。為什麼 Apple 不想讓我們構建自己的 tab bar controllers 和 navigation controllers?或者更確切地說,這段程式碼有什麼問題:
[viewControllerA.view addSubView:viewControllerB.view]
複製程式碼
UIWindow 作為一個應用程式的根檢視(root view),是旋轉和初始佈局訊息等事件產生的來源。在上圖中,child view controller 的 view 插入到 root view controller 的檢視層級中,被排除在這些事件之外了。View 事件方法諸如 viewWillAppear: 將不會被呼叫。
在 iOS 5 之前構建自定義的 view controller 容器時,要儲存一個 child view controller 的引用,還要手動在 parent view controller 中轉發所有 view 事件方法的呼叫,要做好非常困難。 幸運的是, 在 iOS5 之後, Apple 完善了以 viewController 來作為容器的 API, 具體來說, 新增了以下屬性和方法:
@property(nonatomic,readonly) NSArray *childViewControllers;
- (void)addChildViewController:(UIViewController *)childController;
- (void)removeFromParentViewController;
- (void)willMoveToParentViewController:(UIViewController *)parent;
- (void)didMoveToParentViewController:(UIViewController *)parent;
- (void)transitionFromViewController:(nonnull UIViewController *) toViewController:(nonnull UIViewController *) duration:(NSTimeInterval) options:(UIViewAnimationOptions) animations:^(void)animations completion:^(BOOL finished)completion;
複製程式碼
基本上看名字就知道是用來幹嘛的, 簡單總結如下:
- addChildiewController:
新增子控制器的方法, 之後會自動呼叫 willMoveToParentViewController: superVC - removeFromParentViewController:
移除子控制器的方法, 之後會自動呼叫 didMoveToParentViewController: nil - willMoveToParent 當像父 VC 新增子 VC 之後, 該方法會自動呼叫. 若要從父 VC 移除子 VC, 需要在移除之前呼叫該方法, 傳入引數 nil.
- didMoveToParentViewController:
當向父 VC 新增子 VC 之後, 該方法不會被自動呼叫, 從父 VC 移除子 VC 之後, 該方法會自動呼叫, 傳入的引數為 nil. - transitionFromViewController:(nonnull UIViewController *) toViewController:(nonnull UIViewController *) duration:(NSTimeInterval) options:(UIViewAnimationOptions) animations:^(void)animations completion:^(BOOL finished)completion;
切換子檢視的控制器, 同時 View 顯示的內容也會更新, 注意兩個控制器必須已經新增到父控制器中. 在呼叫這個方法前要呼叫[fromViewController willMoveToParentViewController], 在 completion中, 呼叫 [toViewController didMoveToParentViewController: self];
但事實上, 上面某些呼叫是可以省略的. 示例程式碼如下:
新增子控制器:
JVSTestTableVC *TESTVC = [[JVSTestTableVC alloc] init];
// 新增子控制器,必須寫,否則此方法結束, TESTVC就被釋放了
[self addChildViewController:TESTVC];
// [TESTVC willMoveToParentViewController:self]; 自動呼叫, 省略
// [TESTVC didMoveToParentViewController:self]; 可省略
複製程式碼
移除子控制器:
JVSTestVC *childVC = self.childViewControllers.firstObject;
[childVC willMoveToParentViewController:nil];
[childVC removeFromParentViewController];
// [_TESTChildVC didMoveToParentViewController:nil]; 自動呼叫, 省略
NSLog(@"還剩%lu個控制器", self.childViewControllers.count); //列印為0
複製程式碼
總結
總的來說, 堅持要更好地使用和簡化 ViewController 只需要讓他專注於他該做的事情, 併合理運用他的輔助------childViewControllers, 不要讓他去處理不屬於他的事物, 即可寫出整潔清晰的 ViewController 的程式碼. 以上內容部分參考了 Objc 中國的期刊內容, 其中摻雜了很多我個人的理解和一些處理思路. 並給出了新的 demo檔案.
完整的示例程式碼:github.com/JVSFlipped/…