RAC+MVVM,不同VC(VM)间数据双向绑定

RAC搭配MVVM,使用过程中,虽然所有的逻辑处理都放在VM中了,但是当页面之间数据有交互的时候最先想到的还是利用VC属性去传递,我……自我检讨。
业务需求是A页面PUSH到B页面,在B页面做的修改更新到A页面,再次进入B页面也要把A页面的数据带进去。这些数据包括并不在UI上显示的数据,比如数据ID,只用来做数据请求,这样的数据放在VM中最合适,不应该放在VC中,于是我觉得,应该绑定两个VM,这样一来要么需要B页面持有A页面的VM,要么使用router来获取。
第一种方法显然不合适,第二种方法虽然解了耦合,但是明显增加了router的工作,本身VC就要做router,现在VM也要做router就……容易乱,而且这样的方法也不利于双向绑定。
于是写了一个类专门给这样的两个VM属性进行双向绑定,绑定的数据为两个VM属性名称相同的内容,其中一个有变化,另一个跟着变化。

使用方法:在第一个VM中[[SRouterManager shareManager] s_bindViewModel:self.viewModel forkey:keyBindViewModelPublishProductCategory];第二个VM中:[[SRouterManager shareManager] s_bindSecondViewModel:self.viewModel forkey:keyBindViewModelPublishProductCategory];保证key相同就行,需要在页面销毁的时候remove掉[[SRouterManager shareManager] s_removeBindObjectForKey:keyBindViewModelPublishProductCategory];

  • 以下为具体实现,付上router管理VC的方法:
    .h
//
//  SRouterManager.h
//  Snatch
//
//  Created by DawnWang on 2020/3/11.
//  Copyright © 2020 Dawn Wang. All rights reserved.
//

#import 

NS_ASSUME_NONNULL_BEGIN

FOUNDATION_EXPORT NSString *routerScheme;
@interface SRouterManager : NSObject

+ (instancetype)shareManager;
+ (void)releaseManager;


/// push进入一个新的页面
/// @param classString 页面类名称
/// @param params 页面参数
+ (void)s_pushVCClassString:(NSString *)classString withParams:(NSDictionary *__nullable)params;

/// push进入一个新的页面
/// @param viewController 当前VC
/// @param classString 页面类名称
/// @param params 页面参数
+ (void)s_viewController:(UIViewController *__nullable)viewController pushVCClassString:(NSString *)classString withParams:(NSDictionary *__nullable)params;


/// present进入一个新的页面,默认添加srootnavigationcontroller
/// @param classString 页面类名
/// @param params 页面参数
+ (void)s_presentVCClassString:(NSString *)classString withParams:(NSDictionary *__nullable)params;

/// present进入一个新的页面,默认添加srootnavigationcontroller
/// @param viewController 当前VC
/// @param classString 页面类名称
/// @param params 页面参数
+ (void)s_viewController:(UIViewController *__nullable)viewController presentVCClassString:(NSString *)classString withParams:(NSDictionary *__nullable)params;

/// 绑定viewmodel
/// @param viewModel 第一个viewmodel
/// @param key 关键字
- (void)s_bindViewModel:(id)viewModel forkey:(NSString *)key;

/// 绑定viewmodel
/// @param viewModel 第二个viewmodel
/// @param key 需要跟第一个关键字相同,两个model才会绑定在一起,绑定的内容为两个viewmodel属性名相同的property属性
- (void)s_bindSecondViewModel:(id)viewModel forkey:(NSString *)key;

/// 删除某一个绑定
/// @param key key值
- (void)s_removeBindObjectForKey:(NSString *)key;

@end

NS_ASSUME_NONNULL_END

.m

//
//  SRouterManager.m
//  Snatch
//
//  Created by DawnWang on 2020/3/11.
//  Copyright © 2020 Dawn Wang. All rights reserved.
//

#import "SRouterManager.h"
#import 

#import 
#import "NSObject+TopVC.h"

#import "SRootNavigationViewController.h"

NSString *routerScheme = @"Snatch://";

static NSString *const firstViewModelKey = @"firstViewModelKey";
static NSString *const secondViewModelKey = @"secondViewModelKey";

@interface SRouterManager()

@property (nonatomic, strong) JLRoutes *router;
@property (nonatomic, strong) NSMutableDictionary *viewModelsDic;
@property (nonatomic, weak) UIViewController *topVC;
@property (nonatomic, weak) NSDictionary *commenParameters;

@end

@implementation SRouterManager

static SRouterManager *manager = nil;
static dispatch_once_t onceToken;
+ (instancetype)shareManager {
    dispatch_once(&onceToken, ^{
        manager = [[SRouterManager alloc] init];
        [manager s_registerCommenVCs];
    });
    return manager;
}
+ (void)releaseManager {
    manager = nil;
    onceToken = 0;
}

#pragma mark - privit method
- (void)s_registerCommenVCs {
    [[JLRoutes routesForScheme:routerScheme] addRoute:@"push/:viewController" handler:^BOOL(NSDictionary * _Nonnull parameters) {
        NSString *vcString = parameters[@"viewController"];
        UIViewController *pushedVC = [[NSClassFromString(vcString) alloc] init];
        [parameters enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            if ([[self.commenParameters allKeys] containsObject:key]) {
                [pushedVC setValue:obj forKey:key];
            }
        }];
        UIViewController *topVC = self.topVC?:[self s_topViewController];
        [topVC.navigationController pushViewController:pushedVC animated:YES];
        return YES;
    }];
    [[JLRoutes routesForScheme:routerScheme] addRoute:@"present/:viewController" handler:^BOOL(NSDictionary * _Nonnull parameters) {
         NSString *vcString = parameters[@"viewController"];
         UIViewController *presnetVC = [[NSClassFromString(vcString) alloc] init];
         [self.commenParameters enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
             [presnetVC setValue:obj forKey:key];
         }];
         SRootNavigationViewController *nav = [[SRootNavigationViewController alloc] initWithRootViewController:presnetVC];

         UIViewController *topVC = self.topVC?:[self s_topViewController];
         [topVC presentViewController:nav animated:YES completion:nil];
         return YES;
     }];
}

#pragma mark - vc router

+ (void)s_pushVCClassString:(NSString *)classString withParams:(NSDictionary *__nullable)params{
    [SRouterManager s_viewController:nil pushVCClassString:classString withParams:params];
}

+ (void)s_viewController:(UIViewController *__nullable)viewController pushVCClassString:(NSString *)classString withParams:(NSDictionary *__nullable)params{
    [[SRouterManager shareManager] setTopVC:viewController];
    [[SRouterManager shareManager] setCommenParameters:params];
    [[JLRoutes routesForScheme:routerScheme] routeURL:[NSURL URLWithString:[NSString stringWithFormat:@"push/%@",classString]] withParameters:params];
}


+ (void)s_presentVCClassString:(NSString *)classString withParams:(NSDictionary *__nullable)params {
    [SRouterManager s_viewController:nil presentVCClassString:classString withParams:params];
}

+ (void)s_viewController:(UIViewController *__nullable)viewController presentVCClassString:(NSString *)classString withParams:(NSDictionary *__nullable)params {
    [[SRouterManager shareManager] setTopVC:viewController];
    [[SRouterManager shareManager] setCommenParameters:params];
    [[JLRoutes routesForScheme:routerScheme] routeURL:[NSURL URLWithString:[NSString stringWithFormat:@"present/%@",classString]] withParameters:params];
}

#pragma mark - viewModel router
/*
 @{
 连接两个model的key:@{第一个model对象:viewModel,第二个model对象的数组:@[viewModel1,viewModel2,viewModel3]}
 }
 */
- (void)s_bindViewModel:(id)viewModel forkey:(NSString *)key {
    NSMutableDictionary *dic = self.viewModelsDic[key];
    if (dic) {
        [dic setObject:viewModel forKey:firstViewModelKey];
    }else{
        dic = [[NSMutableDictionary alloc] init];
        [dic setObject:viewModel forKey:firstViewModelKey];
        self.viewModelsDic[key] = dic;
    }
}
- (void)s_bindSecondViewModel:(id)viewModel forkey:(NSString *)key {
    NSMutableDictionary *dic = self.viewModelsDic[key];
//    NSAssert(dic, @"请先设置第一个需要绑定的viewmodel");
    NSMutableArray *secondViewModels = dic[secondViewModelKey];
    if (secondViewModels) {
        __block BOOL hasOne = NO;
        __block NSInteger hasIndex = 0;
     //此处是为了清空已经失效的VM对象
        [secondViewModels enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            if ([NSStringFromClass([obj class]) isEqualToString:NSStringFromClass([viewModel class])]) {
                hasOne = YES;
                hasIndex = idx;
                *stop = YES;
            }
        }];

        if (hasOne) {
            [secondViewModels removeObjectAtIndex:hasIndex];
        }
        [secondViewModels addObject:viewModel];
        [self s_beginBindWithKey:key];
    }else{
        secondViewModels = [[NSMutableArray alloc] init];
        [secondViewModels addObject:viewModel];
        self.viewModelsDic[key][secondViewModelKey] = secondViewModels;
        [self s_beginBindWithKey:key];
    }

}

- (void)s_beginBindWithKey:(NSString *)key {
    NSMutableDictionary *dic = self.viewModelsDic[key];
    if (dic) {
        id firstViewModel = dic[firstViewModelKey];
        if (!firstViewModel) {
            return;
        }
        NSArray *secondViewModels = dic[secondViewModelKey];
        NSAssert(firstViewModel && secondViewModels, @"需要绑定的viewmodel都为不能为空");
       //获取第一个viewmodel的属性名称数组
        unsigned firstPropertyCount;
        objc_property_t *firstPropertyList = class_copyPropertyList([firstViewModel class], &firstPropertyCount);
        NSMutableArray *fistViewModelPropertyNames = [NSMutableArray array];
        for (int i = 0; i < firstPropertyCount; i++) {
            objc_property_t property = firstPropertyList[i];
            NSString *propertyName = [NSString stringWithFormat:@"%s",property_getName(property)];
            [fistViewModelPropertyNames addObject:propertyName];
        }

        [secondViewModels enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            //获取第二个viewmodel的属性名称数组
            unsigned propertyCount;
            objc_property_t *propertyList = class_copyPropertyList([obj class], &propertyCount);
            for (int i = 0; i < propertyCount; i++) {
                objc_property_t property = propertyList[i];
                NSString *propertyName = [NSString stringWithFormat:@"%s",property_getName(property)];
                if ([fistViewModelPropertyNames containsObject:propertyName]) {
                    //如果跟第一个viewmodel的属性名称相同就绑定一下
                    [[RACKVOChannel alloc] initWithTarget:obj keyPath:propertyName nilValue:nil][@"followingTerminal"] = [[RACKVOChannel alloc] initWithTarget:firstViewModel keyPath:propertyName nilValue:nil][@"followingTerminal"];
                }
            }
        }];
    }

}
- (void)s_removeBindObjectForKey:(NSString *)key {
    NSAssert(key, @"key不能为空");
    [self.viewModelsDic removeObjectForKey:key];
}

#pragma mark - getter && setter
- (NSMutableDictionary *)viewModelsDic {
    if (!_viewModelsDic) {
        _viewModelsDic = [[NSMutableDictionary alloc] init];
    }
    return _viewModelsDic;
}
@end

虽然不太确定这样写是否符合MVVM的理念,但是我利用这种方法做到了模块间完全解耦。目前没有发现这样写有什么不利于项目的地方,如果有路过的大神指点一下,非常感谢!

你可能感兴趣的:(RAC+MVVM,不同VC(VM)间数据双向绑定)