iOS架構入門 - MVC模式例項演示

小蠢驢打程式碼發表於2018-12-19

image.png

MVC模式的目的是實現一種動態的程式設計,使後續對程式的修改和擴充套件簡化,並且使程式某一部分的重複利用成為可能。除此之外,此模式通過對複雜度的簡化,使程式結構更加直觀

  • 控制器(Controller)--> 負責轉發請求,對請求進行處理。
  • 檢視(View) --> 介面設計人員進行圖形介面設計。
  • 模型(Model) --> 程式設計師編寫程式應有的功能(實現演算法等等)、資料庫專家進行資料管理和資料庫設計(可以實現具體的功能)。

以上出自維基百科資料,下面說點人為描述(簡單易懂的)~

  • Model層: 資料處理層,包括網路請求,資料加工
  • View層: 所有App上看得到的介面
  • Controller層: Model 與 View層的中介,把Model資料在View上展示出來
  • 目的: 低耦合,可複用

image

先看這張圖,這張圖是iOS的MVC架構中最經常出現的圖了吧,因為IOS中的ControlllerUIViewController,所以導致很多人會把檢視寫在Controller中,如下圖:

@implementation DemoViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    //setupUI

    //1.createView
    UIView *view = [[UIView alloc]init];
    view.frame = CGRectMake(100, 100, 100, 100);
    view.backgroundColor = [UIColor orangeColor];
    [self.view addSubview:view];
    
    //2.createButton
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeInfoDark];
    btn.center = self.view.center;
    [self.view addSubview:btn];
    
    //3...
}
複製程式碼

這種寫法在我剛蹭到iOS的時候也這樣寫過,先說這樣寫的好處,以及初學者為什麼會這麼寫:

  1. 比如按鈕,可以在當前控制器直接add target:新增點選事件,在當前控制器內就能呼叫到點選方法,不需要設定代理之類的;
  2. 比如要找某個介面,直接切到這個介面對應的controller就行,因為View 寫在 Controller裡面,不用去別的地方找,就這裡有;
  3. 比如一個View,裡面有一張圖片,圖片依賴於網路資源,這樣寫的好處,可以直接讓 ViewController 中就能拿到資源,不需要傳值

缺點!!:

  • 導致Controller特別臃腫,裡面程式碼特別多,檢視一複雜起來,程式碼量可能過1000行,不好維護
  • 寫在Controller裡無法複用,除非你在 VC2裡面 copy 當前VC中的 View的程式碼
  • 特別low!!會被懂架構的人瞧不起,噴你根本不是MVC,是MC架構,可能還要你來段喊麥證明一下自己(-。-)

如何告別MC模式,真正走到MVC

  1. 先給自己洗腦,iOSController不是UIViewController,而是普通的Controller,沒有View。(很關鍵的一步)
  2. 模組化劃分,每個模組對應自己的一個View,例如Demo2模組,View層裡面有個Demo2View,將介面元素寫到View中

知識1:如何傳值(引數)

//View
+ (instancetype)viewWithTitleStr:(NSString *)titleStr{
    
    //do createView
    //...
}

//Controller
@implementation DemoViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    /*setupUI*/
    //1.createView - 引數通過`View`的函式作為外部引數傳進去
    DemoView *view = [DemoView viewWithTitleStr:@"我是引數"];
    [self.view addSubview:view];
}
複製程式碼

知識2:控制元件點選事件如何回撥給控制器

//View
@implementation DemoView

- (instancetype)initWithTitleStr:(NSString *)titleStr{
    if (self = [super init]) {
        
        UIButton *btn = [UIButton buttonWithType:UIButtonTypeInfoDark];
        [self addSubview:btn];
        [btn addTarget:self action:@selector(p_clickBtn:) forControlEvents:UIControlEventTouchUpInside];
    }
    return self;
}

- (void)p_clickBtn:(UIButton *)sender{
    
    //通過代理回撥
    [_delegate respondsToSelector:@selector(clickBtn:)] ?
    [_delegate clickBtn:sender] : nil;
}

//Controller
@implementation DemoViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    //setupUI
    
    //1.createView
    DemoView *view = [DemoView viewWithTitleStr:@"我是引數"];
    view.delegate = self;
    [self.view addSubview:view];
}

#pragma mark - privateDelegate
- (void)clickBtn:(UIButton *)sender{
    //View層按鈕的點選事件回撥~
}
複製程式碼


image
接下來看這張iOS MVC架構圖二,這張也是特別常見,在上面解決了View層之後,我們來看下這裡的Model層~

@implementation DemoViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    //loadDatas
    [[AFHTTPSessionManager manager]GET:url
                            parameters:parameters
                              progress:nil
                               success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject)
    {
        
        //重新整理tableView
        _datas = responseObject;
        [_tableView reloadDatas];
        
    } failure:nil];
}
複製程式碼

這種寫法在我剛蹭到iOS的時候又這樣寫過,先說這樣寫的好處,以及初學者為什麼會這麼寫:

  1. 簡單,網路請求完,直接在當前控制器重新整理TableView的資料來源
  2. 比如要找某個介面的網路請求,直接切到這個介面對應的controller就行,因為資料請求 寫在 Controller裡面,不用去別的地方找,就這裡有;
  3. 比如當前網路請求介面,需要外部引數,比如前一個介面的uuid,這樣寫的好處,可以直接讓當前請求在 Controller 中就能拿到資源,不需要傳值

缺點!!:

  • 又導致Controller特別臃腫,裡面程式碼特別多,如果當前控制器需要多次請求,程式碼量可能過1000行,不好維護
  • 寫在Controller裡無法複用,除非你在 VC2裡面 copy 當前VC中的 網路請求的程式碼
  • 如果某些介面有依賴要求,介面1請求完再請求介面2,巢狀起來,辣眼睛的程度差點治好我多年的近視
  • 特別low!!會被懂架構的人瞧不起,噴你根本不是MVC,如果你還用了上面的View寫在Controller的操作的話,恭喜你,最終大法 - Controller架構順利完成,並不需要什麼Model && View
    image

如何告別VC模式,真正走到MVC

  1. 不用洗腦,給自己一個大耳刮子讓自己清醒清醒,這iOSController就算是UIViewController,也沒看到M啊,沒有Model。(很關鍵的一步)
  2. 模組化劃分,每個模組對應自己的一個Model,例如Demo2模組,View層裡面有個Demo2Model,將網路請求&&資料處理寫到Model

知識1:如何傳值(引數)

@implementation DemoModel

+ (NSArray *)fetchDatasWithUUid:(NSString *)uuid{

    //Model傳送網路請求
    NSDictionary *parameters = @{@"uuid":uuid}
        [[AFHTTPSessionManager manager]GET:url
                                parameters:parameters
                                  progress:nil
                                   success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject)
        {
            //這是非同步請求,無法return array
        } failure:nil];
    
}
複製程式碼

知識2:如何回撥(網路請求是非同步請求) - 通過Block

//Model
@implementation DemoModel

+ (void)fetchDatasWithUUid:(NSString *)uuid success:(successBlock)block{

    //Model傳送網路請求
    NSDictionary *parameters = @{@"uuid":uuid}
        [[AFHTTPSessionManager manager]GET:url
                                parameters:parameters
                                  progress:nil
                                   success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject)
        {
            //通過block非同步回撥~
            block(responseObject);
    
        } failure:nil];   
}

//Controller
@implementation DemoViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    //loadDatas
    [DemoModel fetchDatasWithUUid:_uuid success:^(NSArray *array) {
        _datas = array;
        [_tableView reloadDatas];
    }];
}
複製程式碼

基礎的MVC講解完畢,其實本質上就是讓Controller減壓,不該控制器管的他別讓他知道,如上基礎MVC操作之後的優勢:

  • MVC架構分明,在同一個模組內,如果檢視有問題,找到該模組的View就行,其他同理,Controller程式碼大大減少,負責View的代理事件就可以
  • 可以複用,比如你一個產品列表的資料,首頁也要用,產品頁也要用,直接分別在其對應的VC1 && VC2 呼叫函式[ProductModel fetchDatas]即可,無需寫多次,View的複用同理
  • 結構分明,便於維護,擴充也是在此基礎上擴充,程式碼乾淨簡潔。

進階講解 - MVC 配合 繼承,進階提高效率

  • 常用的方法,抽一個基類出來,繼承是子類可以擁有父類的方法,重新父類的方法即可,無需宣告
//資料基類
@interface MNBaseDatas : NSObject

//請求資料成功
typedef void (^MNsuccessBlock)(NSArray *array);

+ (void)fetchDatasSuccessBlock:(MNsuccessBlock)block;

+ (void)fetchDatasSuccessBlock:(MNsuccessBlock)block
                  failureBlock:(MNfailureBlock)failure;
複製程式碼

如果,如果抽出一個資料模型的基類,比如這裡的MNBaseDatas,如之前我們舉例的DemoModel就無需宣告

@interface DemoModel : MNBaseDatas

/**繼承自MNBaseDatas,父類有的就可以不用宣告,這裡的block 和 類方法都可以不用宣告*/
//typedef void (^successBlock)(NSArray *array);

//+ (void)fetchDatasSuccessBlock:(MNsuccessBlock)block;

@end

//Controller
@implementation DemoViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    //loadDatas - DemoModel沒有宣告 -fetchDatasSuccessBlock,一樣可以呼叫,因為父類有此方法
    [DemoModel fetchDatasSuccessBlock:^(NSArray *array) {
        _datas = array;
        [_tableView reloadDatas];
    }];
}
複製程式碼

如果父類沒有的方法或屬性,在子類裡面寫就行了,不會影響到父類程式碼,父類一般也是放公共,常用的方法(或屬性),如果是特殊的,直接在子類裡面新增即可,無需新增到父類~


控制器也可以使用繼承,可以減少不少冗餘程式碼

//基類控制器
@interface MNBaseViewController : UIViewController

@property (nonatomic, weak)UITableView *tableView;

@property (nonatomic, copy)NSArray *datas;

- (void)setupUI;

- (void)loadDatas;

@end

//MNBaseViewController.m 檔案
@interface MNBaseViewController ()
<
UITableViewDelegate,UITableViewDataSource
>

#pragma mark - setupUI
- (void)setupUI{
     //統一建立tableView,設定當前代理=self
    UITableView *tableView = [[UITableView alloc]init];
    tableView.frame = Frame(0, DefaultNaviHeight, ScreenW, ScreenH - DefaultNaviHeight);
    tableView.delegate = self;
    tableView.dataSource = self;
}
複製程式碼
  • 根據我們的封裝,基本上所有的控制器都需要設定介面 setupUI 獲取資料 loadDatas,所以將這兩個函式抽到基類MNBaseViewController
  • 因為iOS中,tableView應該算最常見的控制元件之一,基本上大多數介面都會用它展示資料,所以tableView也抽到基類中,當公告屬性
  • tableView 就跑不了資料來源了,datas 同理,也抽到基類
  • 同時,設定MNBaseViewController成為 tableViewdelegatedataSource,所有的子類都無需再宣告
  • 如果有需要用到tableView的,一個[super setUI]就能擁有這個tableView,無需建立

這樣,所有的UIViewController,只要繼承自MNBaseViewController的,都可以有如上的函式和方法(可以根據需要擴充)


進階的MNBaseViewController

//繼承自`MNBaseViewController`
/*資料結構是 - @[],沒有section的tableVIew*/
@interface MNBaseControllerTypeNoSection : MNBaseViewController

@end

/*資料結構是 - @[@[]],有section的tableVIew*/
@interface MNBaseControllerTypeHadSection : MNBaseViewController

@end

複製程式碼
//實現
/**沒有section的tableVIew**/
@implementation MNBaseControllerTypeNoSection

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    
//沒有section,直接返回資料來源count
    return self.datas.count;
}
@end

/**有section的tableVIew**/
@implementation MNBaseControllerTypeHadSection

-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{

    return self.datas.count;
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return [self.datas[section] count];
}

@end

複製程式碼

如上面的兩個基類MNBaseControllerTypeHadSection,MNBaseControllerTypeNoSection,根據我們自己需要的資料來源,選擇繼承自哪個類,他們擁有父類MNBaseViewController的所有屬性,他們的子類,也都無需在寫比如-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView之類的方法等,大幅度減少冗餘程式碼~

demo示例0.png

demo示例1.png

demo示例2.png

demo示例3.png

如圖我隨機抽了幾個介面出來,可能有部分人都有做過類似的介面,通過合理的架構,大部分控制器程式碼可能也就100行,詳情可見Demo~



總結

對於架構來說,仁者見仁智者見智,每個人都有一套適合自己的,並不是說MVC有多low,MVVM甩用MVC 技術10086條街,主要還是根據專案,根據自己的使用慢慢進階。

下面有我一個最近花了幾個小時抽出來的Demo,當然實際開發中的,可能Controller的程式碼會多一些,因為有些點選事件的程式碼我都是封裝呼叫的,再放進去感覺很容易讓看的人跑偏,所以點選事件基本都注掉了。但是,秉著這種思想,其實我最近寫了一個多重過濾袋滑動多控制器的介面,介面相對來說比較複雜,控制器程式碼也才200行,總的來說還算乾淨。

其實TableView也可以剝離到外部,不放在Controller中,我也有Demo是那麼做的,後來發現沒必要,感覺還特意封出去感覺有點畫蛇添足,因為我這種架構,其實tableView很多方法都在基類控制器裡面的,所以Controller中的tableView程式碼也不會多。

MVC架構實戰-Demo


歡迎star~

相關文章