【iOS】KVO+KVC 构建 MVVM

理解MVVM

MVVMMVC 的构建方式很类似,甚至能够说在同一个项目中同时使用这两种架构都不会有任何违和感。MVVM 能够看做是 MVC 的衍生版,其承担 MVC 架构下的 Controller 的一部分职责,这部分职责也就是 ViewModel 所须要作的事情。在 MVVMModelView 之间的通讯,是经过 ViewModel 构建的一条数据管道,ViewModelView 所要展现的 Model 层的数据,转化为最终所须要的版本,View 直接来拿展现。固然这种管道的构建最好经过响应式框架: ReactiveObjcRxSwift。 一样,两种架构一样都是 Controller 充分了解程序各组件,并将他们构建和链接起来。但相比起 MVCMVVM 有如下几点不一样:git

  • ModelViewModel 持有,并非 Controller
  • 需创建起 ViewModelView 之间的绑定关系

本文不会使用响应式框架构建绑定关系,而是经过原生API:KVO+KVC 的方式构建。github

功能封装

  • Controller 基类

    众所周知,在 MVVM 架构中,Controller 是需持有 ViewModel 的。因此构建基类,创建一个 ViewModel 属性是很是有必要的。这样全部继承自 基类 Controller 的子控制器都会拥有 ViewModel架构

    @interface MVVMGenericsController<ViewModelType: id<ViewModelProtocol>> : UIViewController
    
    @property (nonatomic, strong) ViewModelType viewModel;
    
    @end
    复制代码

    首先,基类 Controller 是泛型的(鉴于 Objective-C 中泛型的功能不想 Swift 那么强大,这里仅仅起到个标记的做用,帮助编译器推断 ViewModel 类型),暂且叫它 MVVMGenericsController ,其 ViewModel 类型须要实现 ViewModelProtocol 协议,暂且忽略这个协议,目前来讲,不会对阅读代码产生任何影响。其次,定义了 viewModel 属性。app

  • 绑定时机

    上文说到,MVVM 的关键在于构建 ViewModelView 之间的管道,创建绑定关系。既然这样,能够在 Controller 中设定一个自动回调方法,在某个时机将其触发并在方法当中构建绑定关系。框架

    - (void)bind:(id<ViewModelProtocol>)viewModel {     }
    复制代码

    那么,在什么时候触发这个方法呢?在触发 bind: 方法以前,须要肯定 ViewViewModel 都不为空(这里的 View 指代,须要显示数据的控件,如 Controller 中的 UITableView,ViewModelProtocol 协议后面会讲到),由于须要在这个方法中创建绑定关系,因此必须保证两者是有值的。通常来讲,控制器中子控件的建立,是放在 - (void)viewDidLoad 或者 - (void)loadView 方法里面,因此能够在这两个方法以后调用的 - (void)viewWillAppear:(BOOL)animated 响应 bind: 方法。固然,在每一个控制器中都去手动添加 [self bind] 这样的代码,无疑很麻烦。能够经过 iOS 黑魔法:hook 操做实现自动调用。dom

    @implementation UIViewController (Binding)
    
    + (void)load {
         [self hookOrigInstanceMenthod:@selector(viewWillAppear:) newInstanceMenthod:@selector(mvvm_viewWillAppear:)];
    }
    
    - (void)mvvm_viewWillAppear:(BOOL)animated {
       [self mvvm_viewWillAppear:animated];
    
       if (!self.isAlreadyBind) {
            if ([self isKindOfClass:[MVVMGenericsController class]]) {
                objc_msgSend((MVVMGenericsController *)self, @selector(bindTransfrom));
            }   
           self.isAlreadyBind = YES;
        }
    }
    
    - (void)setIsAlreadyBind:(BOOL)isAlreadyBind {
        objc_setAssociatedObject(self, &kIsAlreadyBind, @(isAlreadyBind), OBJC_ASSOCIATION_ASSIGN);
    }
    
    - (BOOL)isAlreadyBind {
        return !(objc_getAssociatedObject(self, &kIsAlreadyBind) == nil);
    }
    
    - (void)bindTransfrom {}
    
    @end
    复制代码

    首先, hook 操做是在扩展当中实现的。在 + (void)load 方法当中将自定义的方法和系统的 viewWillAppear: 交换。+ (void)load 是在程序编译加载阶段由系统调用,而且只会调用一次,而且在 main 函数以前。故这里是部署 hook 最理想的地方。其次,在这个扩展当中关联了 isAlreadyBind 属性,目的使一个 Controller 在销毁以前只触发一次 bind: 方法。再次,经过 isKindOfClass 判断当前类是否是 MVVMGenericsController 的子类,若是是,就发送 bindTransfrom 消息,bindTransfrom 仅仅是个空方法,不出意外,永远不会调用到这里,它仅仅是让编译器不出现让人厌烦的黄色警告。mvvm

  • MVVMGenericsController 的实现部分

    MVVMGenericsController 才是实现 bindTransfrom: 的地方,由于它才是被真正发出的消息。函数

    @implementation MVVMGenericsController
    
    - (void)bindTransfrom {
        if ([self conformsToProtocol:@protocol(ViewBinder)] && [self respondsToSelector:@selector(bind:)]) {
            if ([self.viewModel conformsToProtocol:@protocol(ViewModelProtocol)]) {
            [((id <ViewBinder>)self) bind:self.viewModel];
                return;
            }
        }
    }
    
    @end
    复制代码

    <ViewBinder> 协议提供了上文提到的,- (void)bind:(id<ViewModelProtocol>)viewModel 方法ui

    @protocol ViewBinder <NSObject>
    
    - (void)bind:(id<ViewModelProtocol>)viewModel; 
    
    @end
    复制代码

    首先会判断当前控制器是否实现了 ViewBinder 协议而且是否能响应 bind: 方法,若是能则派发 bind: ,参数是 ViewModelViewModel 的赋值是在控制器的自定义构造方法中,或者在 - (void)viewWillAppear: 以前。一旦没有在合适的位置赋值,这里会是 nilatom

  • 实现绑定接口

    这里的绑定功能是响应式的,经过观察属性的改变当即获得反馈。固然,经过代理也能够实现,但响应式无疑是最轻量级的。在这里是借助 KVOController + 系统原生API KVC 实现的。一个对象的某个属性被观察后,一旦它发生值的改变,当即将它的结果经过 KVC 赋值给另外一个对象的某一个属性,这便是创建绑定的过程。这里给 NSObject 扩展一些方法:

    @implementation NSObject (Binder)
    
    - (void)bind:(NSString *)sourceKeyPath to:(id)target at:(NSString *)targetKeyPath {
        [self.KVOController observe:self keyPath:sourceKeyPath options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionInitial block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
            id newValue = change[NSKeyValueChangeNewKey];
            if ([self verification:newValue]) {
                [target setValue:newValue forKey:targetKeyPath];
            }
        }];
    }
    
    - (BOOL)verification:(id)newValue {
     if ([newValue isEqual: [NSNull null]]) {
         return NO;
      }
      return YES;
    }
    
    @end
    复制代码

    sourceKeyPath: 被观察对象属性的 keyPathtarget: 目标对象,即被观察到的值赋值给的对象、at:目标对象的属性 keyPath。在 Objective-C 中没有没有像 Swift 当中的 \Foo.barKeyPath 功能,因此这里的键路径只能是字符串。

实现一个案例

  • ViewModel

    毫无疑问,ViewModelMVVM 的核心部件。一个复杂功能的模块,ViewModel 可能会有很大篇幅的代码。ViewModel 应包含一个功能模块的大部分业务逻辑,一个具备交互功能的页面,无疑须要状态的支持。因此 ViewModel 将数据加工好后经过 State 抛出给外部。另外一部分,外部经过 Action 通知 ViewModel 须要作的事情。

    因此,一个 ViewModel 主要由两部分组成 ActionState

    @interface DemoViewModel : NSObject<ViewModelProtocol> // 只是个空协议
    
    // Action
    - (void)changeTitle;
    
    // State
    @property (nonatomic, copy, readonly) NSString *title;
    
    // Model
    @property (nonatomic, copy, readonly) NSArray *titleArray;
    
    @end
    复制代码

    注意:这里的 title(也就是 State )是 readonly 的,要严格采用这种方式,由于一个 State 仅仅是 只读 的就够了。

    @interface DemoViewModel()
    
    @property (nonatomic, copy, readwrite) NSString *title;
    
    @end
    
    @implementation DemoViewModel
    
    - (instancetype)init {
          self = [super init];
          if (self) {
              _titleArray = @[@"MVC", @"MVVM", @"SWift", @"ReactNative"];
             _title = _titleArray[1];
          }
         return self;
    }
    
    - (void)changeTitle {
          self.title = _titleArray[[self randomFloatBetween:0 andLargerFloat:4]];
    }
    
    @end
    复制代码

    ViewModel 的实现部分中将 title 重置为 readwrite ,由于要经过 changeTitle(也就是 Action)改变 title 的值。

  • Controller

    Controller 的职责是将各组件链接起来,在这里构建起 View <-> ViewModel 的管道。

    @interface DemoViewController : MVVMGenericsController<DemoViewModel *><ViewBinder>
    
    @end
    复制代码

    首先,将 MVVMGenericsController 做为父类,因 MVVMGenericsController 中定义了泛型 ViewModelType ,在这里须要指定 ViewModel 的具体类型 <DemoViewModel *>。其次,实现了 <ViewBinder> 协议,该协议提供 - (void)bind:(DemoViewModel *)viewModel 方法。

    @interface DemoViewController ()
    
    @property (nonatomic, strong) UILabel *titleLabel;
    
    @end
    
    @implementation DemoViewController
    
    - (void)bind:(DemoViewModel *)viewModel {
         [viewModel bind:@"title" to:self.titleLabel at:@"text"];
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
         [self.viewModel changeTitle];
    }
    
    复制代码

    - (void)bind:(DemoViewModel *)viewModel 方法中,创建了 ViewModeltitletitleLabeltext 的绑定关系,在这里真正将 ViewModelView 的管道打通。

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 方法中,调用了 ViewModel- (void)changeTitle 方法,目的是改变 title 的值,而一旦 title 值改变,bind: 方法就会监听到值的改变而且将 新的值 赋值给 titleLabel.text。这样就造成了一个单向的数据信息流动。以下图:

    一个原则:State 的改变需经过 Action

    到此为止,一个简单的 MVVM 搭建完毕。固然,能够有不少的 State 也能够有不少的 Action 。只要遵照这个规则,一个 响应式单向数据流 的应用就诞生了。

解除引用循环

很不幸的说,[viewModel bind:@"title" to:self.titleLabel at:@"text"]; 这段代码会产生一个引用循环:viewModel 经过 KVO 观察了本身的 title 属性。这样 KVOController 没法自动移除观察者,因此要手动移除,固然,这个过程是在背后操做的:

const void* const kIsCallPop = &kIsCallPop;

@implementation UIViewController (RetainCircle)

+ (void)load {
    [self hookOrigInstanceMenthod:@selector(viewDidDisappear:) newInstanceMenthod:@selector(mvvm_viewDidDisappear:)];
}

- (void)mvvm_viewDidDisappear:(BOOL)animated {
    [self mvvm_viewDidDisappear:animated];
    
    if ([objc_getAssociatedObject(self, kIsCallPop) boolValue]) {
        if ([self isKindOfClass:[MVVMGenericsController class]] && [((MVVMGenericsController *)self).viewModel conformsToProtocol:@protocol(ViewModelProtocol)]) {
            NSObject *vm = ((MVVMGenericsController *)self).viewModel;
            [vm.KVOController unobserveAll];
        }
    }
}

@end

@implementation UINavigationController (RetainCircle)

+ (void)load {
    [self hookOrigInstanceMenthod:@selector(popViewControllerAnimated:) newInstanceMenthod:@selector(mvvm_popViewControllerAnimated:)];
}

- (UIViewController *)mvvm_popViewControllerAnimated:(BOOL)animated {
    UIViewController* popViewController = [self mvvm_popViewControllerAnimated:animated];
    objc_setAssociatedObject(popViewController, kIsCallPop, @(YES), OBJC_ASSOCIATION_RETAIN);
    return popViewController;
}
复制代码

一样是经过方法交换,很简单,代码不解释了。

结束

经过阅读这篇文章,对 MVVM 是否有了一个全新的认识呢?固然这套代码还有不少不完善的地方,但不影响阅读,不影响对代码的理解。我想这样就够了。

就是这些,这里是 Demo

相关文章
相关标签/搜索