動態

詳情 返回 返回

MVC / MVP / MVVM 架構解析 - 動態 詳情

認真對待每時、每刻每一件事,把握當下、立即去做。

MVC 模式的目的是實現一種動態的程序設計,使後續對程序的修改和擴展簡化,並且使程序某一部分的重複利用成為可能。除此之外,此模式通過對複雜度的簡化,使程序結構更加直觀。下面主要對 MVC 架構下的優化方案以及其項目結構解析。

img

一. MVC 相應層應該做什麼?

1. 控制器(Controller)業務層

控制器(Controller)-->業務層, Model 與 View 層的中介,負責轉發請求,對請求進行處理,把 Model 數據在 View 上展示出來。

主要職責:

  • 管理 View Container 的生命週期;
  • 負責生成所有的 View 實例,並放入 View Container;
  • 監聽來自 View 與業務有關的事件,通過與 Model 的合作,來完成對應事件的業務;

2. 視圖(View)展現層

視圖(View) -->展現層,承載 UI 展示和事件響應(交互)。

主要職責:

  • 響應與業務無關的事件,並因此引發動畫效果,點擊反饋(如果合適的話,儘量還是放在 View 去做)等。
  • 界面元素表達;

3. 模型(Model)數據層

模型(Model) -->數據層,數據處理層,包括網絡請求,數據加工,算法實現等。

主要職責:

  • 給 ViewController 提供數據;
  • 給 ViewController 存儲數據提供接口;
  • 提供經過抽象的業務基本組件,供 Controller 調度;

img

4. 示例解析

在 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...
}

這種寫法在我剛學習編程的時候也這樣寫過,先説這樣寫的好處,以及初學者為什麼會這麼寫:

  • 比如按鈕,可以在當前控制器直接 add target: 添加點擊事件,在當前控制器內就能調用到點擊方法,不需要設置代理之類的;
  • 比如要找某個界面,直接切到這個界面對應的 controller 就行,因為View 寫在 Controller 裏面,不用去別的地方找就這裏有;
  • 比如一個 View,裏面有一張圖片,圖片依賴於網絡資源,這樣寫的好處,可以直接讓 ViewController 中就能拿到資源,不需要傳值;

缺點:

  • 導致 Controller 特別臃腫,裏面代碼特別多,視圖一複雜起來,代碼量可能過1000行,不好維護;
  • 寫在 Controller 裏無法複用,除非你在 VC2 裏面 copy 當前 VC 中的 View 的代碼;
  • 特別low!!會被懂架構的人瞧不起,噴你根本不是 MVC,是 MC 架構;

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

先給自己洗腦,iOSController 不是 UIViewController,而是普通的 Controller,沒有 View。(很關鍵的一步)。

模塊化劃分,每個模塊對應自己的一個 View,例如 Demo 模塊,View 層裏面有個 DemoView,將界面元素寫到 View 中。

二. MVC 相應層之間如何通信?

img

1. View 層和 Controller 層雙向通信

1.1 Controller 如何將數據傳遞到 View 層

  • 創建 View 的時候通過 View 的函數作為外部參數傳進去。

1.2 View 層(用户事件)如何傳遞到 Controller 層

1.2.1 代理(delegate)

通過代理(delegate),代理委託模式通過定義協議方法實現解耦, View 只關心事件觸發不處理具體邏輯;

// 1. 定義協議
@protocol CustomViewDelegate <NSObject>
- (void)customView:(UIView *)view didTapButton:(UIButton *)button;
@end

// 2. View 持有 delegate 弱引用
@interface CustomView : UIView
@property (nonatomic, weak) id<CustomViewDelegate> delegate;
@end

@implementation CustomView
- (void)buttonTapped:(UIButton *)sender {
    [self.delegate customView:self didTapButton:sender]; // 觸發代理方法
}
@end

// 3. Controller 實現協議
@interface ViewController () <CustomViewDelegate>
@end

@implementation ViewController
- (void)viewDidLoad {
    CustomView *view = [[CustomView alloc] init];
    view.delegate = self; // 設置代理
}

- (void)customView:(CustomView *)view didTapButton:(UIButton *)button {
    NSLog(@"Delegate: 按鈕點擊事件處理"); // Controller 響應事件
}
@end

1.2.2 target-action 監聽

在 Controller 設置 target-action 監聽,Controller 給 View 添加一個 target,當用户的觸摸事件發生時,view 產生 action,Controller 接收到之後做出相應的響應,直接建立 View 與控制器的響應鏈關係,適合簡單控件事件;

// 1. View 暴露添加 target 的方法
@interface CustomView : UIView
- (void)addTarget:(id)target action:(SEL)action;
@end

@implementation CustomView {
    id _target;
    SEL _action;
}

- (void)addTarget:(id)target action:(SEL)action {
    _target = target;
    _action = action;
}

- (void)buttonTapped {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [_target performSelector:_action withObject:self]; // 執行 Action
    #pragma clang diagnostic pop
}
@end

// 2. Controller 設置 Target-Action
@implementation ViewController
- (void)viewDidLoad {
    CustomView *view = [[CustomView alloc] init];
    [view addTarget:self action:@selector(handleButtonTap:)]; // 綁定事件
}

- (void)handleButtonTap:(CustomView *)sender {
    NSLog(@"Target-Action: 按鈕點擊事件處理"); // Controller 響應事件
}
@end

1.2.3 數據源模式 data source

通過數據源模式 data source,通過數據驅動 UI 更新,控制器實現數據獲取協議供 View 調用;

// 1. 定義數據源協議
@protocol CustomViewDataSource <NSObject>
- (NSString *)textForButtonInView:(CustomView *)view;
@end

// 2. View 持有 dataSource 引用
@interface CustomView : UIView
@property (nonatomic, weak) id<CustomViewDataSource> dataSource;
- (void)reloadData; // 觸發數據更新
@end

@implementation CustomView
- (void)reloadData {
    NSString *text = [self.dataSource textForButtonInView:self]; // 獲取數據
    [_button setTitle:text forState:UIControlStateNormal];
}
@end

// 3. Controller 實現數據源
@interface ViewController () <CustomViewDataSource>
@end

@implementation ViewController
- (void)viewDidLoad {
    CustomView *view = [[CustomView alloc] init];
    view.dataSource = self;
    [view reloadData]; // 初始化數據
}

- (NSString *)textForButtonInView:(CustomView *)view {
    return @"DataSource 模式"; // 提供動態數據
}
@end
1.2.4 Block(閉包)

Block(閉包):‌View 定義閉包屬性,Controller 通過賦值閉包來響應事件。‌優點,代碼緊湊,適合簡單回調。‌缺點,需注意循環引用(使用 [weak self])。

class CustomView: UIView {
    var onButtonTap: (() -> Void)?
    @objc func buttonTapped() { onButtonTap?() }
}
// Controller 中賦值
customView.onButtonTap = { [weak self] in self?.handleTap() }

2. Model 層和 Controller 層雙向通信

我們來看下這裏的 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];
}

這種寫法在我剛學習編程的時候也這樣寫過,先説這樣寫的好處,以及初學者為什麼會這麼寫:

  • 簡單,網絡請求完,直接在當前控制器刷新 TableView 的數據源;
  • 比如要找某個界面的網絡請求,直接切到這個界面對應的 controller 就行,因為數據請求 寫在 Controller 裏面,不用去別的地方找,就這裏有;
  • 比如當前網絡請求接口,需要外部參數,比如前一個界面的 uuid,這樣寫的好處,可以直接讓當前請求在 Controller 中就能拿到資源,不需要傳值;

缺點:

  • 又導致 Controller 特別臃腫,裏面代碼特別多,如果當前控制器需要多次請求,代碼量可能過1000行,不好維護;
  • 寫在 Controller 裏無法複用,除非你在 VC2 裏面 copy 當前 VC 中的 網絡請求的代碼;
  • 如果某些接口有依賴要求,接口1請求完再請求接口2,需要嵌套起來;
  • 特別 low!!會被懂架構的人瞧不起,噴你根本不是 MVC,如果你還用了上面的 View 寫在 Controller 的操作的話,恭喜你,最終大法 - Controller 架構 順利完成,並不需要什麼 Model && View

iOSController 就算是 UIViewController,也沒看到 Model 啊,沒有 Model。(很關鍵的一步);

模塊化劃分,每個模塊對應自己的一個 Model,例如 Demo 模塊,Model 層裏面有個 DemoModel,將網絡請求&&數據處理寫到 Model 中;

2.1 Controller 調用和傳值到 Model

Controller 層直接調用 Model 層類方法和實例方法,並通過參數傳值。

2.2 Model 層數據如何回調到 Controller 層

Model 層數據如何回調到 Controller 層,Controller 層如何知道 Model 層數據發生了改變。

2.2.1 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];
    }];
}
2.2.2 KVO(監聽)

KVO(監聽),監聽 Model 的每個屬性的變化來做出響應;

// Model.h
@interface MyModel : NSObject
@property (nonatomic, strong) NSString *data;
@end

// Controller.m
- (void)viewDidLoad {
    [super viewDidLoad];
    [self.model addObserver:self 
                 forKeyPath:@"data" 
                    options:NSKeyValueObservingOptionNew 
                    context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object 
                        change:(NSDictionary *)change 
                       context:(void *)context {
    if ([keyPath isEqualToString:@"data"]) {
        self.label.text = change[NSKeyValueChangeNewKey]; // 響應變化
    }
}

- (void)dealloc {
    [self.model removeObserver:self forKeyPath:@"data"];
}
2.2.3 Notification(通知)

Notification(通知),Model 中創建一個 NSNotificationCenter,在 Controller 中創建一個方法來接收通知。當 Model 發生變化時,他會發送一個通知,而 Controller 會接收通知,一對多廣播式通信,適合跨模塊解耦但性能開銷較大。

img

解釋一下上面這幅圖,一個完整的模塊被分為了三個相對獨立的部分,分別是Model,View,Controller,對應到我們 App 中的依次為繼承自 NSObject 的數據中心,承載 UI 展示和事件響應的 View 以及我們最最常用的 UIViewController。

其中 VC 持有 View 和 Model 部分,View 通過代理或者 Target-Action 的方式把用户的操作傳遞給 VC,VC 負責根據不同的用户行為做出不同響應。如果需要加載或刷新數據則直接調用 Model 暴露的接口,如果數據可以同步拿到,則直接使用獲取到的數據刷新 View。如果數據需要通過網絡請求等其他異步的方式獲取,VC 則通過監聽 Model 發出的數據更新(成功或失敗)通知,在收到通知時根據成功或者失敗對 View 進行相應的刷新操作。可以看出來整個過程中 View 和 Model 是沒有直接交互的,所有的操作都是通過 VC 進行協調的。

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

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

三. MVC 架構常見的疑惑

1. 遺失的網絡邏輯(網絡數據請求應該放在那裏?)

蘋果使用的 MVC 的定義是這麼説的:所有的對象都可以被歸類為一個 Model,一個 View,或是一個控制器。就這些,那麼把網絡代碼放哪裏?和一個 API 通信的代碼應該放在哪兒?

你可能試着把它放在 Model 對象裏,但是也會很棘手,因為網絡調用應該使用異步,這樣如果一個網絡請求比持有它的 Model 生命週期更長,事情將變的複雜。顯然也不應該把網絡代碼放在 View 裏,因此只剩下控制器了。這同樣是個壞主意,因為這加劇了厚重控制器的問題。那麼應該放在那裏呢?顯然 MVC 的 3 大組件根本沒有適合放這些代碼的地方。

網絡請求與數據處理的歸屬爭議:

  1. ‌純數據模型派:
    認為 Model 應僅定義數據結構,網絡請求和數據處理應由 Controller 或單獨的服務類(如 NetworkManager)處理。

  2. ‌增強 Model 派:

    支持將網絡請求封裝在 Model 內部,通過擴展方法或靜態函數實現,例如:

    extension NGLoginModel {
        static func fetchAccount(completion: @escaping (NGLoginModel?) -> Void) {
            NetworkManager.request(url: "api/login") { data in
                let account = NetcallAccount(data: data)
                completion(NGLoginModel(info: account))
            }
        }
    }
    

    這種方式保持數據與獲取邏輯的緊密性,但可能增加 Model 的複雜度。

》https://www.jianshu.com/p/309f0477aac1

四. MVP / MVVM

1. MVP / MVVM 解析

這裏引用優秀博客,歡迎大家去學習:https://www.jianshu.com/p/b5043499b096

2. MVVM + RxSwift

這裏我用一個示例來説如何使用 MVVM + RxSwift,對於具體 RxSwfit 詳細內容這裏不做解析。

數據模型‌:定義了電影數據結構,遵循 Decodable 協議以便從 JSON 解析。

import Foundation

struct Movie: Decodable {
    let title: String
    let overview: String
    let posterPath: String
    let releaseDate: String
    
    enum CodingKeys: String, CodingKey {
        case title
        case overview
        case posterPath = "poster_path"
        case releaseDate = "release_date"
    }
}

網絡服務層‌:使用 Alamofire 進行網絡請求,返回 RxSwift 的 Observable 對象。

import RxSwift
import Alamofire

class MovieAPI {
    static let shared = MovieAPI()
    private let apiKey = "YOUR_API_KEY" // 替換為實際API Key
    
    func fetchPopularMovies() -> Observable<[Movie]> {
        let url = "https://api.themoviedb.org/3/movie/popular"
        let parameters: [String: Any] = [
            "api_key": apiKey,
            "language": "en-US"
        ]
        
        return Observable.create { observer in
            AF.request(url, parameters: parameters)
                .validate()
                .responseDecodable(of: MovieResponse.self) { response in
                    switch response.result {
                    case .success(let movieResponse):
                        observer.onNext(movieResponse.results)
                        observer.onCompleted()
                    case .failure(let error):
                        observer.onError(error)
                    }
                }
            return Disposables.create()
        }
    }
}

struct MovieResponse: Decodable {
    let results: [Movie]
}

視圖模型‌:包含業務邏輯,使用 BehaviorRelay 存儲數據狀態,處理加載和錯誤狀態。

import RxSwift
import Alamofire

class MovieAPI {
    static let shared = MovieAPI()
    private let apiKey = "YOUR_API_KEY" // 替換為實際API Key
    
    func fetchPopularMovies() -> Observable<[Movie]> {
        let url = "https://api.themoviedb.org/3/movie/popular"
        let parameters: [String: Any] = [
            "api_key": apiKey,
            "language": "en-US"
        ]
        
        return Observable.create { observer in
            AF.request(url, parameters: parameters)
                .validate()
                .responseDecodable(of: MovieResponse.self) { response in
                    switch response.result {
                    case .success(let movieResponse):
                        observer.onNext(movieResponse.results)
                        observer.onCompleted()
                    case .failure(let error):
                        observer.onError(error)
                    }
                }
            return Disposables.create()
        }
    }
}

struct MovieResponse: Decodable {
    let results: [Movie]
}

視圖控制器:負責 UI 展示,通過 RxSwift 綁定 ViewModel 數據到 UI 控件。

import RxSwift
import Alamofire

class MovieAPI {
    static let shared = MovieAPI()
    private let apiKey = "YOUR_API_KEY" // 替換為實際API Key
    
    func fetchPopularMovies() -> Observable<[Movie]> {
        let url = "https://api.themoviedb.org/3/movie/popular"
        let parameters: [String: Any] = [
            "api_key": apiKey,
            "language": "en-US"
        ]
        
        return Observable.create { observer in
            AF.request(url, parameters: parameters)
                .validate()
                .responseDecodable(of: MovieResponse.self) { response in
                    switch response.result {
                    case .success(let movieResponse):
                        observer.onNext(movieResponse.results)
                        observer.onCompleted()
                    case .failure(let error):
                        observer.onError(error)
                    }
                }
            return Disposables.create()
        }
    }
}

struct MovieResponse: Decodable {
    let results: [Movie]
}

這個示例完整展示了 MVVM 架構在 iOS 中的實現,RxSwift 的使用使得數據綁定和異步操作更加簡潔高效:

  • Model 層‌:Movie 和 MovieAPI 負責數據處理;
  • ViewModel 層‌:MovieListViewModel 處理業務邏輯;
  • View 層‌:MovieListViewController 負責 UI 展示;
user avatar chengxuyuanlaoliu2024 頭像
點贊 1 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.