Don't let your UIViewControl

作者: zuoming | 来源:发表于2015-09-28 13:57 被阅读200次

    翻译自Don't let your UIViewController think for itself。是作者的这一系列三篇文章中的第二篇。

    上篇文章,我展示了如何将协议(protocol)从view controller分离出来,以达到为胖controller瘦身的目的。在本系列三篇文章的第二篇,我将使用MVVM架构来精简我们的View controllerView controller通常会包含非常多的业务逻辑,这会导致非常多的问题,比如:

    • 代码将会难以阅读。让View controller负责外观,而将业务逻辑封装在单独对象里。
    • View controller将会变得非常臃肿。然而业务逻辑本不应该放在View controller里。

    MVVM?

    MVVMModel View ViewModel 的缩写,是一种将界面、业务逻辑与数据分离的方法,并以一种相互独立的方式结合在一起。
    这种方法的工作方式就像这张图展示的一样:

    MVVM-1.png
    我认识的一个聪明家伙是这样描述它的:

    “...数据从Model传递给ViewModel,经过ViewModel处理之后,再发送给View。交互事件与数据修改从View传递给ViewModel,经过ViewModel处理之后,再发送给Model。”

    我本来可以说出一大堆使用MVVM的好处(比如测试、代码的可读性等等),但这已经超过了本文的探讨范畴。当然,如果你对此非常感兴趣,objc.io上的这篇非常棒的文章可以供你阅读。

    特征蠕动

    假设我们现在有了一个非常棒的view controller,而且在一个完美的世界里我们不再需要修改它了。如果真这样的话,我们程序员可就都要失业了。

    回归现实,产品经理刚刚给你提了一个新的需求。Joe Public非常想在点击列表或数据加载完成的时候修改导航栏的标题。

    非常简单对不对?我们只需要把这段代码写在...view controller里...?不!这是坏码农的做法!

    一个好的码农,今天要做的就是把业务逻辑从view controller中分离出来。那么业务逻辑应该放哪呢?我很高兴地告诉你:放在ViewModel里。

    ViewModel

    如果你已经知道MVVM或者看了之前提到的那篇文章,那么你应该已经知道什么是ViewModel了。在MVVMViewModel是传递给view controller的一个对象,内部实现了所有有关view controler的业务逻辑和事件处理,是数据操作的大脑。

    迁移到ViewModel里

    OK,回到我们的工程里,首先我们需要创建一个名为VCTableModelViewModel类,继承于NSObject。在开始实现新功能之前,我们先把view controller已经存在的业务逻辑放进我们新创建的ViewModel中,看见没?就是MyAPI!代码如下:

    //VCTableModel.h
    @class MyAPI;
    
    typedef void(^didReloadDataBlock)(NSArray *data);  
    typedef void(^didErrorBlock)(NSString *error);
    
    @interface VCTableModel : NSObject
    -(instancetype)initWithManager:(MyAPI *)api;
    -(void)reloadData;
    @property (nonatomic, copy) didReloadDataBlock didReloadData;
    @property (nonatomic, copy) didErrorBlock didError;
    @end
    

    在这段代码中,你会发现我们是通过构造方法将MyAPI对象传给VCTableModel。这项技术被称为 依赖注入,非常适合在 MVVM中使用。如果你对依赖注入,可以阅读这篇文章

    使用MVVM的一个最大的好处就是,如果运用得当的话,可以使view controller难以置信地随着数据的变化而自动变化。而这一神奇的特性是通过在view controllerViewModel之间创建一种绑定关系实现的。

    有很多工具可以实现这种绑定关系,比如ReactiveCocoaikevents,而在这里我仅仅通过block来实现。可以发现我已经在** VCTableModel**中定义了两个事件, 一个用来处理出错,另一个用来处理数据更新。最后我们还有一个方法用来在view controller中加载数据的。

    接口已经定义好了,接下来该实现它们了,打开VCTableModel.m,为我们的MyAPI创建一个私有属性:

    @property (nonatomic, strong) MyAPI *api;
    

    添加构造方法:

    -(instancetype)initWithManager:(MyAPI *)api {
        if (!(self = [super init])) { return nil; }
        self.api = api;
        return self;
    }
    

    Next up let's add the methods that will 'push' our events and a small helper to change an NSError into an NSString

    接下来添加一个传递事件的方法,而且在传递事件之前,我们会把NSError转化为NSString的形式:

    -(NSString *)userMessageForError:(NSError *)error {
        return [NSString stringWithFormat:@"An Error Occurred\n\n%@", error.localizedDescription];
    }
    -(void)sendError:(NSError *)error {
      if (self.didError == nil) { return; }
    
      dispatch_async(dispatch_get_main_queue(), ^{
        self.didError([self userMessageForError:error]);
      });
    }
    -(void)sendData:(NSArray *)data {
        if (self.didReloadData == nil) { return; }
    
        dispatch_async(dispatch_get_main_queue(), ^{
            self.didReloadData(data);
        });
    }
    

    干得漂亮!现在只剩下一件事要做了:将调用MyAPI的代码移植过来,因为MyAPI已经不再与界面直接藕合了。

    -(void)reloadData {
        [self.api getPhotos:^(NSArray *json) {
            //Convert JSON items into our model objects
            NSMutableArray *items = [NSMutableArray array];
            for (NSDictionary *jsonItem in json) {
                VCTableCellData *item = [[VCTableCellData alloc] initWithJSON:jsonItem];
                [items addObject:item];
            }  
    
            //Raise didReloadData
            [self sendData:items];
    
        } error:^(NSError *error) {
            //Something went wrong, raise didError
           [self sendError:error];
        }];
    }
    

    好了,就是这样!现在功能都已经放在ViewModel里了,随时可以传递给view controller

    迁移View Controller

    移除老的代码

    既然我们已经创建了ViewModel,接下来应该使用它来更新我们的view controller了。首先我们在view controller中把移入ViewModel的代码删除。

    执行以下步骤:

    • 修改title的更新方式并且删除self.api = [MyAPI new];

    • 删除#import "MyAPI.h"

    • 删除私有属性 MyAPI *api

    • 使用以下代码替换现有的reloadData方法:

      -(void)reloadData {
        [self.refresh beginRefreshing];
        [self.model reloadData];
      }
      

    目前先暂时忽略编译错误,我们马上就会修复它。

    引入新的代码

    First thing we need to do is provide an instance of VCTableModel to our view controller via the constructor:
    我们需要做的第一件事就是为我们的view controller 的构造方法添加一个VCTableModel参数:

    //VCTable.h  
    @class VCTableModel;
    
    @interface VCTable : UIViewController
        -(instancetype)initWithModel:(VCTableModel *)model;
    @end
    

    接下来为view controller 添加一个私有的ViewModel属性:

    #import "VCTableModel.h"
    
    @property (nonatomic, strong) VCTableModel *model;
    

    新的构造方法实现如下:

    -(instancetype)initWithModel:(VCTableModel *)model {
        if (!(self = [super initWithNibName:NSStringFromClass([self class]) bundle:nil])) 
        { 
            return nil; 
        }
        self.model = model;
        return self;
    }
    

    差不多了!下一步我们添加一个方法用来将ViewModel中的事件与view controller绑定在一起:

    -(void)bindToModel {
        self.model.didError = [self modelDidError];
        self.model.didReloadData = [self modelDidReloadData];
    }
    -(didErrorBlock)modelDidError {
        return ^(NSString *error) {
            [self endRefreshing];
    
            [[[UIAlertView alloc]
              initWithTitle:@"Oops..." message:error 
              delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]
             show];
        };
    }
    -(didReloadDataBlock)modelDidReloadData {
        return ^(NSArray *data) {
            [self endRefreshing];
            [self.tableViewCoordinator reloadData:data];
        };
    }
    

    注意:你可能不太熟悉这种处理事件的语法,但是block可以作为方法的返回值。这样做仅仅是为了提高代码的可阅读性。

    最后务必在方法viewDidLoad中调用 [self bindToModel]

    祝贺你! 你现在已经把你的 view controller 转换为MVVM模式!

    实现新功能!

    现在可以开始添加我们新的功能了!第一步,我们应该把获取view controller标题的方法放进我们的ViewModel里,这样做可以让修改标题变得非常容易。比如,你有一个个人资料的界面,该界面与一个User对象关联,你就可以非常方便地将标题修改为用户(User)的名字,而不需要修改view controller( 这是MVVM的一个关键的原则)。

    所以让我们再次更新ViewModel

    //VCTableModel.h
    @property (readonly) NSString *title;
    
    
    //VCTableModel.m
    -(NSString *)title {
        return @"MVVM Table View Example";
    }
    

    并且修改view controller的** viewDidLoad**方法:self.title = self.model.title;

    我们知道有两种情况需要更新导航栏的标题,我们可以通过以下步骤来实现:

    1. 定义一个事件回调来:

       //VCTableModel.h
       typedef void(^didUpdateNavigationTitleBlock)(NSString *title);
      
      @property (nonatomic, copy) didUpdateNavigationTitleBlock didUpdateNavigationTitle;
      
    2. 定义一个方法来触发这个事件回调:

       //VCTableModel.m
       -(void)sendNavigationTitle:(NSString *)title {
           if (self.didUpdateNavigationTitle == nil) { return; }
      
           dispatch_async(dispatch_get_main_queue(), ^{
               self.didUpdateNavigationTitle(title);
           });
       }
      
    3. 我们还需要一个方法来处理列表的点击事件:

       //VCTableModel.h
       @class VCTableCellData;
       -(void)userSelectedCell:(VCTableCellData *)cellData;
      
       //VCTableModel.m
        -(void)userSelectedCell:(VCTableCellData *)cellData {
           [self sendNavigationTitle:cellData.title];
       }
      

    ViewModel最后需要修改的就是更新方法reloadData:在调用api之前添加[self sendNavigationTitle:self.title];

    1. 在我们的view controller中,添加一个处理事件回调的方法:

       -(didUpdateNavigationTitleBlock)modelDidUpdateNavigationTitle {
           return ^(NSString *title) {
                 self.title = title;
           };
       }
      

    然后在方法bindToModel里添加self.model.didUpdateNavigationTitle = [self modelDidUpdateNavigationTitle];

    1. 最后在方法itemPressed添加:Lastly add [self.model userSelectedCell:data];, 这样我们的model就可以发送这些事件了,并且我们的新功能也完成了!

    扫尾工作

    最后,我喜欢写一个便利方法,可以非常方便地将view controllerViewModel与依赖关系结合在一起。

    创建** VCTable的一个category/extension,取名为VCTable+Factory**:

     //VCTable+Factory.h
    @interface VCTable (Factory)
     +(instancetype)factoryInstance;
    @end
    
    
    //VCTable+Factory.m
    #import "VCTableModel.h"
    #import "MyAPI.h"
    
    @implementation VCTable (Factory)
    +(instancetype)factoryInstance {
        VCTableModel *model = [[VCTableModel alloc] initWithManager:[MyAPI new]];
        return [[VCTable alloc] initWithModel:model];
    }
    @end
    

    AppDelegate使用#import "VCTable+Factory.h"替换原来的#import "VCTable.h"并且更新方法**application:didFinishLaunchingWithOptions: **:

    -(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    
        VCTable *rootViewController = [VCTable factoryInstance];
        self.navigationController = [[UINavigationController alloc] initWithRootViewController:rootViewController];
        self.window.rootViewController = self.navigationController;
        [self.window makeKeyAndVisible];
    
        return YES;
    }
    

    现在你可以开始运行它了。

    瘦而简单的view controller!

    干得好!现在我们的view controller只负责展示界面而不用关心其他事情了。

    在这篇文章里,我向你展示了将业务逻辑从 view controller中剥离出来是如此的简单,并且我希望这篇MVVM入门的文章对我来说会非常简单。

    Grab the source for our MVVM view controller here.
    点此获取本篇相关源代码

    相关文章

      网友评论

        本文标题:Don't let your UIViewControl

        本文链接:https://www.haomeiwen.com/subject/uzoucttx.html