iOS架构升级

大纲

  1. 面临的问题是什么?
  • 解决方案是什么?
  • 如何实施?
  • 效果怎么样?
  • 如何避免重蹈覆辙?

1. 现状

Cocoa的MVC模式驱使人们写出臃肿的视图控制器,因为它们经常被混杂到View的生命周期中,因此很难说View和ViewController是分离的。尽管仍可以将业务逻辑和数据转换到Model,但是大多数情况下当需要为View减负的时候我们却无能为力了,View的最大的任务就是向Controller传递用户动作事件。ViewController最终会承担一切代理和数据源的职责,还负责一些分发和取消网络请求以及一些其他的任务,因此就不难理解苹果为什么给取名ViewController了。

iOS架构升级_第1张图片
1452152425723031.png

在我们项目中可能会看见过很多这样的代码:

    PlantCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    
    if (!cell) {
        cell = [[PlantCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
    }
    
    PlantModel *model = [self.dataSource objectAtIndex:indexPath.row];
    [cell configCellWithModel:model];

这个cell,正是由View直接来调用Model,事实上已经违背了MVC的原则。但是这种情况是一直发生的,甚至于我们不觉得这里有哪些不对。如果严格遵守MVC的话,你会把对cell的设置放在 Controller 中,不向View传递一个Model对象,这样就会大大增加Controller的体积,所以我们的项目中经常看到一个controller代码量超过2000行,实际维护起来非常麻烦。比如要修改一个点击事件,翻了半天终于找到了,定睛一看,竟然是网络请求。

“Cocoa 的MVC被写成Massive View Controller 是不无道理的。”

直到进行单元测试的时候才会发现问题越来越明显。因为你的ViewController和View是紧密耦合的,对它们进行测试就显得很艰难,你得有足够的创造性来模拟View和它们的生命周期,在以这样的方式来写View Controller的同时,业务逻辑的代码也逐渐被分散到View的布局代码中去。这也是业界对iOS开发者普遍不写单元测试的诟病的吐槽之一吧。

2. 该如何入手(MVVM)

简介

MVVM,Model-View-ViewModel,一个从 MVC 模式中进化而来的设计模式,最早于2005年被微软的 WPF 和 Silverlight 的架构师 John Gossman 提出。在 iOS 开发中实践 MVVM 的话,通常会把大量原来放在 ViewController 里的视图逻辑和数据逻辑移到 ViewModel 里,从而有效的减轻了 ViewController 的负担。另外通过分离出来的 ViewModel 获得了更好的测试性,我们可以针对 ViewModel 来测试,解决了界面元素难于测试的问题。MVVM 通常还会和一个强大的绑定机制一同工作,一旦 ViewModel 所对应的 Model 发生变化时,ViewModel 的属性也会发生变化,而相对应的 View 也随即产生变化。

iOS架构升级_第2张图片
MVC模式和MVVM模式的差别

优点

  1. 方便测试。在MVC下,Controller基本是无法测试的,里面混杂了个各种逻辑,而且分散在不同的地方。有了MVVM我们就可以测试里面的viewModel,来验证我们的处理结果对不对。

  2. 便于代码的移植。比如我们运营app和运维app,部分功能除了交互展示不一样外,业务逻辑的model是一致的。这样,我们就可以以很小的代价去开发另一个app。。

  3. 兼容MVC。MVVM是MVC的一个升级版,目前的MVC也可以很快的转换到MVVM这个模式。VC可以省去一大部分展示逻辑。

缺点:

  1. MVVM 的学习成本和开发成本都很高。MVVM 是一个年轻的设计模式,大多数人对它的了解都不如对 MVC 熟悉,基于绑定机制来进行编程需要一定的学习才能较好的上手。同时在 iOS 客户端开发中,并没有现成的绑定机制可以使用,要么使用 KVO,要么引入类似 RxSwift或ReactiveCocoa 这样的第三方库,使得学习成本和开发成本进一步提高,但RxSwift也更能简化代码,这样可以放更多的时间到业务流程开发中。

  2. 数据绑定使 Debug 变得更难了。数据绑定使程序异常能快速的传递到其他位置,在界面上发现的 Bug 有可能是由 ViewModel 造成的,也有可能是由 Model 层造成的,传递链越长,对 Bug 的定位就越困难。

  3. 在传统的 MVVM 架构中,ViewModel 依然承载大量的逻辑,包括业务逻辑,界面逻辑,数据存储和网络相关,使得 ViewModel 仍然有可能变得和 MVC 中 ViewController 一样臃肿。

3. 实施

项目目录结构按照MVVM的分层方式进行了修改,主要划分为View,ViewModel,Model和Service。

![项目目录结构](http://upl
![Uploading Simulator Screen Shot 2017年1月7日 11.45.38_716403.png . . .]
oad-images.jianshu.io/upload_images/925877-f7ddeaa2cd069b64.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

以前的网络请求是单独封装了一个网络请求类工具类,需要调用网络请求的地方到处调用该方法,代码如下

  • 工具类
    + (void)post:(NSString *)url params:(NSDictionary *)params success:(void (^)(id json))success failure:(void (^)(NSError *error))failure {
        if (ISEMPTY(params[@"curPage"])) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [HTLoading showGrayLoading];
            });
        }
    
        [self checkNetwork:failure];
    
        // 2.发送请求
        NSMutableDictionary *mutableParams = [NSMutableDictionary dictionaryWithDictionary:params];
        [mutableParams setValue:HTAPI_APPKEY forKey:@"appkey"];
        [mutableParams setValue:NSLocalizedString(@"Language", nil) forKey:@"language"];
        [mutableParams setValue:CONF_GET(@"token") forKey:@"token"];
    
        AFHTTPSessionManager *sessionManager = [self sharedClient];
        //设置请求头,这些参数根据不同的页面或者不同的网络会发生变化
        [sessionManager.requestSerializer setValue:[mutableParams description] forHTTPHeaderField:@"oper_info"];
        [sessionManager.requestSerializer setValue:url forHTTPHeaderField:@"oper_url"];
        [sessionManager.requestSerializer setValue:[Utils getIPAddress] forHTTPHeaderField:@"login_ip"];
    
        [sessionManager POST:url parameters:mutableParams progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
            NSError *error = [NSError errorWithDomain:CustomErrorDomain code:XDefultFailed userInfo:@{NSLocalizedDescriptionKey:NSLocalizedString(@"返回数据异常", nil)}];
            if (success) {
                [HTLoading hideLoading];
                // 请求成功,返回失败数据
                if (responseObject == nil || [responseObject[@"result_code"] integerValue] != 1) {
                    NSLog(@"error:%@", mutableParams);
    #ifdef DEBUG
                    NSString *info = [NSString stringWithFormat:@"%@:%ld,%@",NSLocalizedString(@"错误代码",nil),(long)error.code, [error.userInfo objectForKey:NSLocalizedDescriptionKey]];
    #else
                    NSString *info = [error.userInfo objectForKey:NSLocalizedDescriptionKey];
    #endif
    
                    ShowToastLong(NSLocalizedString(info, nil));
                    
                    failure(error);
                } else {
                    success(responseObject);
                }
            } else {
                failure(error);
            }
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            NSLog(@"error:%@ param = %@", error, mutableParams);
            [HTLoading hideLoading];
    #ifdef DEBUG
            NSString *info = [NSString stringWithFormat:@"%@:%ld,%@",NSLocalizedString(@"错误代码",nil),(long)error.code, [error.userInfo objectForKey:NSLocalizedDescriptionKey]];
    #else
            NSString *info = [error.userInfo objectForKey:NSLocalizedDescriptionKey];
    #endif
            ShowToastLong(NSLocalizedString(info, nil));
            if (failure) {
                failure(error);
            }
        }];
    }

  • 调用方法
    //调用接口服务请求参数初始化
    NSMutableDictionary *params = [NSMutableDictionary dictionaryWithDictionary:@{@"service":@"getPsList", @"org_id":_org_id, @"user_id":CONF_GET(@"user_id"), @"curPage":[NSString stringWithFormat:@"%ld", (long)_curPage]}];
    NSLog(@"plant_list_req %@",params);
    if(self.sort_name){
        [params setValue:self.sort_name forKey:@"sort_column"];
    }
    if (self.sortType) {
        [params setValue:self.sortType forKey:@"sort_type"];
    }
    
    [HTHttpTool postPathWithParams:params success:^(id json) {
        [self.tableView.mj_header endRefreshing];
        //正常处理数据
        NSMutableArray *tempArray = [HTPlant mj_objectArrayWithKeyValuesArray:[json[@"result_data"] objectForKey:@"pageList"]];
        self.psArray = tempArray;
        if (self.psArray.count < 1) {
            self.emptyView.hidden = NO;
        } else {
            self.emptyView.hidden = YES;
        }
        [self.tableView reloadData];
    } failure:^(NSError *error) {
        [self.tableView.mj_header endRefreshing];
        self.emptyView.hidden = NO;
        self.emptyView.title.text = NSLocalizedString(@"下拉重试", nil);
    }];
  • 然后绑定数据到view上,整个过程相当臃肿。

改进方法

  • 首先定义一个Service,对应着接口的一个Service,比如我们的APPService,我们对应采用相关的service,在该service中我们只需要实现对应的TargetType即可
    import Foundation
    import RxSwift
    import Moya
    import Alamofire
    
    enum AppService {
        case Login(user_account: String, user_password: String, sys_code: String, login_type: String)
        case GetPsList(org_id: String, user_id: String, device_type: String, curPage: String, size: String)
    }
    
    extension AppService: TargetType {
        var baseURL: URL {
            return URL(string: "https://api.isolarcloud.com/sungws")!
        }
        
        var path: String {
            return "/AppService";
        }
        
        var method: Moya.Method {
            return .post
        }
        
        var parameters: [String: Any]? {
            switch self {
            case .Login(let user_account, let user_password, let sys_code, let login_type):
                return ["service": "login", "user_account": user_account, "user_password": user_password, "sys_code": sys_code, "login_type": login_type]
            case .GetPsList(let org_id, let user_id, let device_type, let curPage, let size):
                return ["service": "getPsList", "org_id": org_id, "user_id": user_id, "device_type": device_type, "curPage": curPage, "size": size]
            }
        }
        
        var sampleData: Data {
            switch self {
            case .Login:
                return "".data(using: String.Encoding.utf8)!
            case .GetPsList(_, _, _, _, _):
                return "Create post successfully".data(using: String.Encoding.utf8)!
            }
        }
        
        var task: Task {
            return .request
        }
    }
    
    let headerFields: Dictionary = [
        "User-Agent": "sungrow-agent",
        "system": "iOS",
        "sys_ver": String(UIDevice.version())
    ]
    
    let appendedParams: Dictionary = [
        "appkey": appkey,
        "language": "_zh_CN"
    ]
    
    let endpointClosure = { (target: AppService) -> Endpoint in
        let defaultEndpoint = MoyaProvider.defaultEndpointMapping(for: target)
        return defaultEndpoint.adding(parameters: appendedParams, httpHeaderFields: headerFields, parameterEncoding: JSONEncoding.default)
    }
    
    let appServiceProvider = RxMoyaProvider(endpointClosure: endpointClosure)

  • 在ViewModel中实现网络请求
    import Foundation
    import RxSwift
    import Moya
    
    let defaut_curPage = "1"
    let defaut_page_size = "20"
    
    class ViewModel {
        func login(user_account: String, user_password: String, sys_code: String, login_type: String) -> Observable {
            return appServiceProvider.request(.Login(user_account: user_account, user_password: user_password, sys_code: sys_code, login_type: login_type))
                .mapJSON()
                .mapObject(type: Login.self)
        }
        
        func getPsList(org_id: String, user_id: String, device_type: String, curPage: String = defaut_curPage, size: String = defaut_page_size) -> Observable<[PlantStation]> {
            return appServiceProvider.request(.GetPsList(org_id: org_id, user_id: user_id, device_type: device_type, curPage: curPage, size: size))
                .mapJSON()
                .mapArray(type: PlantStation.self)
        }
    }
  • 在controller中调用ViewModel,绑定到对应的View中即可.
    viewModel.getPsList(org_id: "79", user_id: "179", device_type: "1,4,7", curPage: "1")
            .bindTo(tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (row, model, cell) in
                cell.textLabel?.text = "\(model.ps_name ?? "") @ row \(row)"
            }
            .addDisposableTo(disposeBag)

4. 效果

Demo中采用了MVVM的方式进行了网络的初始化,网络的请求,数据的解析,以及数据的绑定,能够很清晰的找到每一个过程,不再像以前需要找一个网络请求半天找不到再哪里,而且轻松实现实现了数据的请求并显示到页面上

iOS架构升级_第3张图片
请求结果和数据绑定

5. 避免重蹈覆辙

需要深刻理解MVVM架构的分层结构,尽量按照约定的分层进行代码开发。重新思考业务模型,抽象,抽象,在抽象。
  1. view层
    • 具有共性的view单独抽出,避免相同的代码重复拷贝,建立项目的公用控件仓库
  2. 逻辑层
    • 按照业务进行模块划分,一些跟具体业务无关的内容按照工具箱的思路进行封装,比如各种日期选择工具,网络加载等待,每个模块都封装独立的framework。
  3. 数据层
    • 使用Moya网络分层,采用TargetType的protocol。
    • 按照数据存储方式进行模块划分。

你可能感兴趣的:(iOS架构升级)