更好地使用 ViewController

JVSFlipped發表於2018-04-15

前言

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]
複製程式碼

更好地使用 ViewController

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;

複製程式碼

基本上看名字就知道是用來幹嘛的, 簡單總結如下:

  1. addChildiewController:
    新增子控制器的方法, 之後會自動呼叫 willMoveToParentViewController: superVC
  2. removeFromParentViewController:
    移除子控制器的方法, 之後會自動呼叫 didMoveToParentViewController: nil
  3. willMoveToParent 當像父 VC 新增子 VC 之後, 該方法會自動呼叫. 若要從父 VC 移除子 VC, 需要在移除之前呼叫該方法, 傳入引數 nil.
  4. didMoveToParentViewController:
    當向父 VC 新增子 VC 之後, 該方法不會被自動呼叫, 從父 VC 移除子 VC 之後, 該方法會自動呼叫, 傳入的引數為 nil.
  5. 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/…

相關文章