【IOS开发高级系列】MVVM—ReactiveCocoa架构设计专题(一)

2023-10-16 11:37:26 浏览数 (2)

1 MVVM简介

1.1 MVC介绍

        MVC: Massive View Controller. Alot of the time, it’s convenient to put business logic and other code into viewcontrollers, even if that’s not architecturally the most sound place to put it.

        Use of MVVM helps reduce theamount of business logic in view controllers, which helps reduce their bloatedsize and also makes that business logic more testable.

1.2 MVVM要点

        In MVVM, we tend to consider the view andthe view controller as one entity (which explains why it’s not called MVVCVM).Instead of the view controller performing a lot of those mediations betweenmodel and view, the view model does, instead.

        ReactiveCocoa will monitor for changes inthe model and map those changes to properties on the view model, performing anynecessary business logic.

1.3 MVVM示例

        As a concrete example, imagine our modelcontains a date called dateAdded that we want to monitor for changes, andupdate our view model’s dateAdded property. The model’s property is an NSDate instance,while the view model’s is a transformed NSString. The binding would looksomething like the following (inside the view model’s init method).

RAC(self,dateAdded) = [RACObserve(self.model, dateAdded) map:^(NSDate *date) {

    return [[ViewModel dateFormatter] stringFromDate: date];

}];

        dateFormatter is a class method onViewModel that caches an NSDateFormatter instance so it can be reused (they’re expensive to create). Then, the view controller can monitor the view model’s

dateAdded property, binding it to a label.

RAC(self.label, text) = RACObserve(self.viewModel, dateAdded);

        We’ve now abstracted the logic of transforming the date to a string into our view model, where we might write unit tests for it. It seems contrived in this trivial example, but as we’ll see,it helps reduce the amount of logic in your view controllers significantly.

        此处有三个重点是我希望你看完本文能带走的:

• MVVM可以兼容你当下使用的MVC架构。

• MVVM增加你的应用的可测试性。

• MVVM配合一个绑定机制效果最好。

        如我们之前所见,MVVM基本上就是MVC的改进版,所以很容易就能看到它如何被整合到现有使用典型MVC架构的应用中。让我们看一个简单的PersonModel以及相应的View Controller:

@interface Person : NSObject

 @property (nonatomic, readonly) NSString*salutation;

@property (nonatomic, readonly) NSString*firstName;

@property (nonatomic, readonly) NSString*lastName;

@property (nonatomic, readonly) NSDate*birthdate;

 - (instancetype) initwithSalutation: (NSString *)salutation firstName:(NSString*)firstName lastName:(NSString *)lastName birthdate:(NSDate*)birthdate;

@end

        Cool!现在我们假设我们有一个 PersonViewController,在 viewDidLoad里,只需要基于它的 model属性设置一些Label即可。

- (void)viewDidLoad{

    [superviewDidLoad];

    if (self.model.salutation.length > 0) {

        self.nameLabel.text = [NSStringstringWithFormat:@"%@

%@ %@", self.model.salutation, self.model.firstName, self.model.lastName];

    }else{

        self.nameLabel.text = [NSStringstringWithFormat:@"%@

%@", self.model.firstName, self.model.lastName];

    }

    NSDateFormatter *dateFormatter =[[NSDateFormatter alloc] init];

    [dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];

    self.birthdateLabel.text = [dateFormatter stringFromDate: model.birthdate];

}

    这全都直截了当,标准的MVC。现在来看看我们如何用一个View Model来增强它。

@interface PersonViewModel : NSObject

@property (nonatomic, readonly) Person *person;

@property (nonatomic, readonly) NSString*nameText;

@property (nonatomic, readonly) NSString*birthdateText;

-(instancetype) initWithPerson: (Person *)person;

@end

    我们的View Model的实现大概如下:

@implementation PersonViewModel

- (instancetype) initWithPerson: (Person *)person {

    self = [super init];

    if (!self) return nil;

    _person = person;

    if (person.salutation.length > 0) {

        _nameText = [NSString stringWithFormat: @"%@

%@ %@", self.person.salutation, self.person.firstName, self.person.lastName];

    }else{

        _nameText = [NSString stringWithFormat: @"%@ %@", self.person.firstName, self.person.lastName];

    }

    NSDateFormatter *dateFormatter =[[NSDateFormatter alloc] init];

    [dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];

    _birthdateText = [dateFormatter stringFromDate: person.birthdate];

    return self;

}

@end

Cool!我们已经将 viewDidLoad中的表示逻辑放入我们的View Model里了。此时,我们新的 viewDidLoad就会非常轻量:

- (void)viewDidLoad{

    [superviewDidLoad];

    self.nameLabel.text = self.viewModel.nameText;

    self.birthdateLabel.text = self.viewModel.birthdateText;

}

        所以,如你所见,并没有对我们的MVC架构做太多改变。还是同样的代码,只不过移动了位置。它与MVC兼容,带来更轻量的 View Controllers。

        可测试,嗯?是怎样?好吧,ViewController是出了名的难以测试,因为它们做了太多事情。在MVVM里,我们试着尽可能多的将代码移入View Model里。测试ViewController就变得容易多了,因为它们不再做一大堆事情,并且View Model也非常易于测试。让我们来看看:

SpecBegin(Person)

    NSString *salutation = @"Dr.";

    NSString *firstName = @"first";

    NSString *lastName = @"last";

    NSDate *birthdate = [NSDate dateWithTimeIntervalSince1970: 0];

    it (@"should use the salutation available. ", ^{

        Person *person = [[Person alloc] initWithSalutation: salutation firstName: firstName lastName: lastName birthdate: birthdate];

        PersonViewModel *viewModel =[[PersonViewModel alloc] initWithPerson:person];

        expect(viewModel.nameText).to.equal(@"Dr. first last");

    });

    it (@"should not use an unavailable salutation. ", ^{

        Person *person = [[Person alloc] initWithSalutation: nil firstName: firstName lastName: lastName birthdate: birthdate];

        PersonViewModel *viewModel =[[PersonViewModel alloc] initWithPerson: person];

        expect(viewModel.nameText).to.equal(@"first last");

    });

    it (@"should use the correct date format. ", ^{

        Person *person = [[Person alloc] initWithSalutation: nil firstName: firstName lastName: lastName birthdate: birthdate];

        PersonViewModel *viewModel =[[PersonViewModel alloc] initWithPerson: person];

        expect(viewModel.birthdateText).to.equal(@"Thursday January 1, 1970");

    });

SpecEnd

        注意到在这个简单的例子中,Model是不可变的,所以我们可以只在初始化的时候指定我们View Model的属性。对于可变Model,我们还需要使用一些绑定机制,这样View Model就能在背后的Model改变时更新自身的属性。此外,一旦View Model上的Model发生改变,那View的属性也需要更新。Model的改变应该级联向下通过ViewModel进入View。

        在OS X上,我们可以使用Cocoa绑定,但在iOS上我们并没有这样好的配置可用。我们想到了KVO(Key-Value Observation),而且它确实做了很伟大的工作。然而,对于一个简单的绑定都需要很大的样板代码,更不用说有许多属性需要绑定了。作为替代,我个人喜欢使用ReactiveCocoa,但MVVM并未强制我们使用ReactiveCocoa。MVVM是一个伟大的典范,它自身独立,只是在有一个良好的绑定框架时做得更好。

2 ReactiveCocoa

ReactiveCocoa指南一:信号

ReactiveCocoa指南二:Twitter搜索实例

MVVM指南一:Flickr搜索实例

MVVM指南二:Flickr搜索深入

2.1 响应式编程FRP

2.1.1 函数式编程 (FunctionalProgramming)

        函数式编程也可以写N篇,它是完全不同于OO的编程模式,这⾥里主要讲一下这个框架使⽤用到的函数式思想。

        (1) 高阶函数:在函数式编程中,把函数当参数来回传递,而这个,说成术语,我们把他叫做高阶函数。在oc中,blocks是被广泛使⽤用的参数传递,它实际上是匿名函数。

        高阶函数调用过程有点像linux命令⾥里的pipeline(管道),一个命令调用后的输出当作另一个命令输入,多个命令之间可以串起来操作。来个例子:

RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;

// Contains: 22 44 66 88

RACSequence *doubleNumber = [[numbers filter:^ BOOL(NSString *value) {

    return (value.intValue % 2) == 0;

}] map:^id(id value) {

    return [value stringByAppendingString: value];

}];

        上面的例子是数组里的值先进行过滤,得到偶数,然后再将每个值进行stringByAppendingString,最终输出22 44 66 88.

        (2) 惰性(或延迟)求值:Sequences对象等,只有当被使用到时,才会对其求值。

        关于函数编程,有兴趣的大家可以研究下haskell或者clojure,不过目前好多语⾔言都在借用函数式的思想。

2.1.2 响应式编程(Functional Reactive Programming:FRP)

        响应式编程是一种和事件流有关的编程模式,关注导致状态值改变的行为事件,一系列事件组成了事件流。一系列事件是导致属性值发生变化的原因。FRP非常类似于设计模式里的观察者模式。

        响应式编程是一种针对数据流和变化传递的编程模式,其执行引擎可以自动的在数据流之间传递数据的变化。比如说,在一种命令式编程语言中,a: = b c 表示 a 是 b c 表达式的值,但是在RP语言中,它可能意味着一个动态的数据流关系:当c或者b的值发生变化时,a的值自动的发生变化。

        RP已经被证实是一种最有效的处理交互式用户界面、实时模式下的动画的开发模式,但本质上是一种基本的编程模式。现在最为热门的JavaFX脚本语言中,引入的bind就是RP的一个概念实现。

响应式编程其关键点包括:

    (1) 输入被视为"行为",或者说一个随时间而变化的事件流

    (2) 连续的、随时间而变化的值

    (3) 按时间排序的离散事件序列

        FRP与普通的函数式编程相似,但是每个函数可以接收一个输入值的流,如果其中,一个新的输入值到达的话,这个函数将根据最新的输入值重新计算,并且产⽣生一个新的输出。这是一种”数据流"编程模式。

2.2 ReactiveCocoa试图解决什么问题

        经过一段时间的研究,我认为ReactiveCocoa试图解决以下3个问题:

    1、传统iOS开发过程中,状态以及状态之间依赖过多的问题;

    2、传统MVC架构的问题:Controller比较复杂,可测试性差;

    3、提供统一的消息传递机制;

        (1) 开发过程中,状态以及状态之间依赖过多,RAC更加有效率地处理事件流,而无需显式去管理状态。在OO或者过程式编程中,状态变化是最难跟踪,最头痛的事。这个也是最重要的一点。

        (2) 减少变量的使用,由于它跟踪状态和值的变化,因此不需要再申明变量不断地观察状态和更新值。

        (3) 提供统一的消息传递机制,将oc中的通知,action,KVO以及其它所有UIControl事件的变化都进行监控,当变化发生时,就会传递事件和值。

        (4) 当值随着事件变换时,可以使用map,filter,reduce等函数便利地对值进行变换操作。

2.3 试图解决MVC框架的问题

        我们在开发iOS应用时,一个界面元素的状态很可能受多个其它界面元素或后台状态的影响。

        RAC的信号机制很容易将某一个Model变量的变化与界面关联,所以非常容易应用Model-View-ViewModel框架。通过引入ViewModel层,然后用RAC将ViewModel与View关联,View层的变化可以直接响应ViewModel层的变化,这使得Controller变得更加简单,由于View不再与Model绑定,也增加了View的可重用性。

        因为引入了ViewModel层,所以单元测试可以在ViewModel层进行,iOS工程的可测试性也大大增强了。InfoQ也曾撰文介绍过MVVM:《MVVM启示录》 。

2.4 统一消息传递机制

        iOS开发中有着各种消息传递机制,包括KVO、Notification、delegation、block以及target-action方式。各种消息传递机制使得开发者在做具体选择时感到困惑,例如在objc.io上就有专门撰文(破船的翻译 ),介绍各种消息传递机制之间的差异性。

        RAC将传统的UI控件事件进行了封装,使得以上各种消息传递机制都可以用RAC来完成。

2.5 使用时机

2.5.1 (1)处理异步或者事件驱动的数据变化

static void *ObservationContext = &ObservationContext;

- (void)viewDidLoad {

    [super viewDidLoad];

    [LoginManager.sharedManager addObserver: self forKeyPath: @"loggingIn" options: NSKeyValueObservingOptionInitial context: &ObservationContext];

    [NSNotificationCenter.defaultCenter addObserver: self selector: @selector(loggedOut:) name: UserDidLogOutNotification object: LoginManager.sharedManager];

    [self.usernameTextField addTarget: self action: @selector(updateLogInButton) forControlEvents: UIControlEventEditingChanged];

    [self.passwordTextField addTarget: self action: @selector(updateLogInButton) forControlEvents: UIControlEventEditingChanged];

    [self.logInButton addTarget: self action: @selector(logInPressed:) forControlEvents: UIControlEventTouchUpInside];

}

- (void)dealloc {

    [LoginManager.sharedManager removeObserver: self forKeyPath: @"loggingIn" context: ObservationContext];

    [NSNotificationCenter.defaultCenter removeObserver: self];

}

- (void)updateLogInButton {

    BOOL textFieldsNonEmpty =self.usernameTextField.text.length > 0 && self.passwordTextField.text.length > 0;

    BOOL readyToLogIn =!LoginManager.sharedManager.isLoggingIn && !self.loggedIn;

    self.logInButton.enabled = textFieldsNonEmpty &&readyToLogIn;

}

- (IBAction)logInPressed:(UIButton *)sender {

    [[LoginManager sharedManager] logInWithUsername: self.usernameTextField.text password: self.passwordTextField.text success:^{

    self.loggedIn = YES;

    } failure:^(NSError *error) {

        [self presentError: error];

    }];

}

- (void)loggedOut:(NSNotification *)notification {

    self.loggedIn = NO;

}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change: (NSDictionary *)change context:(void *)context {

    if (context == ObservationContext) {

        [self updateLogInButton];

    } else {

        [super observeValueForKeyPath: keyPath ofObject: object change: change context: context];

    }

}

// RAC实现:

- (void)viewDidLoad {

    [super viewDidLoad];

    @weakify(self);

    RAC(self.logInButton, enabled) = [RACSignal combineLatest: @[self.usernameTextField.rac_textSignal, self.passwordTextField.rac_textSignal, RACObserve(LoginManager.sharedManager, loggingIn), RACObserve(self, loggedIn)

] reduce:^(NSString *username, NSString *password,NSNumber *loggingIn, NSNumber *loggedIn) {

    return @(username.length > 0 &&password.length > 0 && !loggingIn.boolValue && !loggedIn.boolValue);

}];

    [[self.logInButton rac_signalForControlEvents: UIControlEventTouchUpInside] subscribeNext:^(UIButton *sender) {

    @strongify(self);

    RACSignal *loginSignal = [LoginManager.sharedManager logInWithUsername: self.usernameTextField.text password: self.passwordTextField.text];

    [loginSignal subscribeError:^(NSError *error) {

        @strongify(self);

        [self presentError: error];

    } completed:^{

        @strongify(self);

        self.loggedIn = YES;

    }];

}];

RAC(self, loggedIn) =[[NSNotificationCenter.defaultCenter rac_addObserverForName: UserDidLogOutNotificationobject: nil] mapReplace: @NO];

}

2.5.2 (2)链式的依赖操作

[client logInWithSuccess:^{

    [client loadCachedMessagesWithSuccess:^(NSArray*messages) {

        [client fetchMessagesAfterMessage: messages.lastObject success:^(NSArray *nextMessages) {

            NSLog(@"Fetched all messages.");

        } failure:^(NSError *error) {

            [self presentError: error];

        }];

    } failure:^(NSError *error) {

        [self presentError: error];

    }];

} failure:^(NSError *error) {

    [self presentError: error];

}];

// RAC实现:

[[[[client logIn] then:^{

    return [client loadCachedMessages];

}] flattenMap:^(NSArray *messages) {

    return [client fetchMessagesAfterMessage: messages.lastObject];

}] subscribeError:^(NSError *error) {

    [self presentError: error];

} completed:^{

    NSLog(@"Fetched all messages.");

}];

2.5.3 (3)并⾏行依赖操作:

__block NSArray *databaseObjects;

__block NSArray *fileContents;

NSOperationQueue *backgroundQueue = [[NSOperationQueuealloc] init];

NSBlockOperation *databaseOperation = [NSBlockOperationblockOperationWithBlock:^{

    databaseObjects = [databaseClient fetchObjectsMatchingPredicate: predicate];

}];

NSBlockOperation *filesOperation = [NSBlockOperationblockOperationWithBlock:^{

    NSMutableArray *filesInProgress = [NSMutableArray array];

    for (NSString *path in files) {

        [filesInProgress addObject:[NSData dataWithContentsOfFile: path]];

    }

    fileContents = [filesInProgress copy];

}];

NSBlockOperation *finishOperation = [NSBlockOperationblockOperationWithBlock:^{

    [self finishProcessingDatabaseObjects: databaseObjects fileContents: fileContents];

    NSLog(@"Done processing");

}];

[finishOperation addDependency: databaseOperation];

[finishOperation addDependency: filesOperation];

[backgroundQueue addOperation: databaseOperation];

[backgroundQueue addOperation: filesOperation];

[backgroundQueue addOperation: finishOperation];

//RAC

RACSignal *databaseSignal = [[databaseClient fetchObjectsMatchingPredicate: predicate] subscribeOn: [RACScheduler scheduler]];

RACSignal *fileSignal = [RACSignal startEagerlyWithScheduler: [RACScheduler scheduler] block:^(id subscriber) {

    NSMutableArray *filesInProgress = [NSMutableArray array];

    for (NSString *path in files) {

        [filesInProgress addObject: [NSData dataWithContentsOfFile: path]];

    }

    [subscriber sendNext: [filesInProgress copy]];

    [subscriber sendCompleted];

}];

[[RACSignal combineLatest: @[ databaseSignal, fileSignal ] reduce:^ id (NSArray *databaseObjects, NSArray*fileContents) {

    [self finishProcessingDatabaseObjects: databaseObjects fileContents: fileContents];

    return nil;

}] subscribeCompleted:^{

    NSLog(@"Done processing");

}];

2.5.4 (4)简化集合操作

NSMutableArray *results = [NSMutableArray array];

for (NSString *str in strings) {

    if (str.length < 2) {

        continue;

    }

    NSString *newString = [str stringByAppendingString: @"foobar"];

    [results addObject: newString];

}

RAC实现:

RACSequence *results = [[strings.rac_sequence filter:^ BOOL (NSString *str) {

    return str.length >= 2;

}] map:^(NSString *str) {

    return [str stringByAppendingString: @"foobar"];

}];

2.6 ReactiveCocoa的特点

        RAC在应用中大量使用了block,由于Objective-C语言的内存管理是基于引用计数的,为了避免循环引用问题,在block中如果要引用self,需要使用@weakify(self)和@strongify(self)来避免强引用。另外,在使用时应该注意block的嵌套层数,不恰当的滥用多层嵌套block可能给程序的可维护性带来灾难。

        RAC的编程方式和传统的MVC方式差异巨大,所以需要较长的学习时间。并且,业界内对于RAC并没有广泛应用,这造成可供参考的项目和教程比较欠缺。 另外,RAC项目本身也还在快速演进当中,1.x版本和2.x版本API改动了许多,3.0版本也正在快速开发中,对它的使用也需要考虑后期的升级维护问题。

        作为一个iOS开发领域的新开源框架,ReactiveCocoa带来了函数式编程和响应式编程的思想,值得大家关注并且学习。

0 人点赞