ReactiveCocoa自述:工作原理和应用

举个例子,一个text field能够绑定到最新状态,即使它在变,而不需要用额外的代码去更新text field每一秒的状态.它有点像KVO,但它用blocks代替了重写-observeValueForKeyPath:ofObject:change:context:.

Signals也能够呈现异步的操作,有点像futures and promises.这极大地简化了异步软件,包括了网络处理的代码.

RAC有一个主要的优点,就是提供了一个单一的,统一的方法去处理异步的行为,包括delegate方法,blocks回调,target-action机制,notifications和KVO.

这里有一个简单的例子:

1
2
3
4
5
6
7
8
// When self.username changes, logs the new name to the console.
//
// RACObserve(self, username) creates a new RACSignal that sends the current
// value of self.username, then the new value whenever it changes.
// -subscribeNext: will execute the block whenever the signal sends a value.
[RACObserve(self, username) subscribeNext:^(NSString *newName) {
     NSLog(@ "%@" , newName);
}];

这不像KVO notifications,signals能够连接在一起并且能够同时进行操作:

1
2
3
4
5
6
7
8
9
10
11
// Only logs names that starts with "j".
//
// -filter returns a new RACSignal that only sends a new value when its block
// returns YES.
[[RACObserve(self, username)
     filter:^(NSString *newName) {
         return  [newName hasPrefix:@ "j" ];
     }]
     subscribeNext:^(NSString *newName) {
         NSLog(@ "%@" , newName);
     }];

Signals也能够用来导出状态.而不是observing properties或者设置其他的 properties去反应新的值,RAC通过signals and operations让表示属性变得有可能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Creates a one-way binding so that self.createEnabled will be
// true whenever self.password and self.passwordConfirmation
// are equal.
//
// RAC() is a macro that makes the binding look nicer.
// 
// +combineLatest:reduce: takes an array of signals, executes the block with the
// latest value from each signal whenever any of them changes, and returns a new
// RACSignal that sends the return value of that block as values.
RAC(self, createEnabled) = [RACSignal 
     combineLatest:@[ RACObserve(self, password), RACObserve(self, passwordConfirmation) ] 
     reduce:^(NSString *password, NSString *passwordConfirm) {
         return  @([passwordConfirm isEqualToString:password]);
     }];

Signals不仅仅能够用在KVO,还可以用在很多的地方.比如说,它们也能够展示button presses:

1
2
3
4
5
6
7
8
9
10
11
12
// Logs a message whenever the button is pressed.
//
// RACCommand creates signals to represent UI actions. Each signal can
// represent a button press, for example, and have additional work associated
// with it.
//
// -rac_command is an addition to NSButton. The button will send itself on that
// command whenever it's pressed.
self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
     NSLog(@ "button was pressed!" );
     return  [RACSignal empty];
}];

或者异步的网络操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Hooks up a "Log in" button to log in over the network.
//
// This block will be run whenever the login command is executed, starting
// the login process.
self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) {
     // The hypothetical -logIn method returns a signal that sends a value when
     // the network request finishes.
     return  [client logIn];
}];
// -executionSignals returns a signal that includes the signals returned from
// the above block, one for each time the command is executed.
[self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {
     // Log a message whenever we log in successfully.
     [loginSignal subscribeCompleted:^{
         NSLog(@ "Logged in successfully!" );
     }];
}];
// Executes the login command when the button is pressed.
self.loginButton.rac_command = self.loginCommand;

Signals能够展示timers,其他的UI事件,或者其他跟时间改变有关的东西.

对于用signals来进行异步操作,通过连接和改变这些signals能够进行更加复杂的行为.在一组操作完成时,工作能够很简单触发:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Performs 2 network operations and logs a message to the console when they are
// both completed.
//
// +merge: takes an array of signals and returns a new RACSignal that passes
// through the values of all of the signals and completes when all of the
// signals complete.
//
// -subscribeCompleted: will execute the block when the signal completes.
[[RACSignal 
     merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]] 
     subscribeCompleted:^{
         NSLog(@ "They're both done!" );
     }];

Signals能够顺序地执行异步操作,而不是嵌套block回调.这个和futures and promises很相似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Logs in the user, then loads any cached messages, then fetches the remaining
// messages from the server. After that's all done, logs a message to the
// console.
//
// The hypothetical -logInUser methods returns a signal that completes after
// logging in.
//
// -flattenMap: will execute its block whenever the signal sends a value, and
// returns a new RACSignal that merges all of the signals returned from the block
// into a single signal.
[[[[client 
     logInUser] 
     flattenMap:^(User *user) {
         // Return a signal that loads cached messages for the user.
         return  [client loadCachedMessagesForUser:user];
     }]
     flattenMap:^(NSArray *messages) {
         // Return a signal that fetches any remaining messages.
         return  [client fetchMessagesAfterMessage:messages.lastObject];
     }]
     subscribeNext:^(NSArray *newMessages) {
         NSLog(@ "New messages: %@" , newMessages);
     } completed:^{
         NSLog(@ "Fetched all messages." );
     }];

RAC也能够简单地绑定异步操作的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Creates a one-way binding so that self.imageView.image will be set as the user's
// avatar as soon as it's downloaded.
//
// The hypothetical -fetchUserWithUsername: method returns a signal which sends
// the user.
//
// -deliverOn: creates new signals that will do their work on other queues. In
// this example, it's used to move work to a background queue and then back to the main thread.
//
// -map: calls its block with each user that's fetched and returns a new
// RACSignal that sends values returned from the block.
RAC(self.imageView, image) = [[[[client 
     fetchUserWithUsername:@ "joshaber" ]
     deliverOn:[RACScheduler scheduler]]
     map:^(User *user) {
         // Download the avatar (this is done on a background queue).
         return  [[NSImage alloc] initWithContentsOfURL:user.avatarURL];
     }]
     // Now the assignment will be done on the main thread.
     deliverOn:RACScheduler.mainThreadScheduler];

这里仅仅说了RAC能做什么,但很难说清RAC为什么如此强大.虽然通过这个README很难说清RAC,但我尽可能用更少的代码,更少的模版,把更好的代码去表达清楚.

如果想要更多的示例代码,可以check outC-41 或者 GroceryList,这些都是真正用ReactiveCocoa写的iOS apps.更多的RAC信息可以看一下Documentation文件夹.

什么时候用ReactiveCocoa

乍看上去,ReactiveCocoa是很抽象的,它可能很难理解如何将它应用到具体的问题.

这里有一些RAC常用的地方.

处理异步或者事件驱动数据源

很多Cocoa编程集中在响应user events或者改变application state.这样写代码很快地会变得很复杂,就像一个意大利面,需要处理大量的回调和状态变量的问题.

这个模式表面上看起来不同,像UI回调,网络响应,和KVO notifications,实际上有很多的共同之处。RACSignal统一了这些API,这样他们能够组装在一起然后用相同的方式操作.

举例看一下下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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表达的话就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (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:UserDidLogOutNotification object:nil]
         mapReplace:@NO];
}

连接依赖的操作

依赖经常用在网络请求,当下一个对服务器网络请求需要构建在前一个完成时,可以看一下下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
[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];
}];

ReactiveCocoa 则让这种模式特别简单:

1
2
3
4
5
6
7
8
9
10
11
12
[[[[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." );
     }];

并行地独立地工作

与独立的数据集并行,然后将它们合并成一个最终的结果在Cocoa中是相当不简单的,并且还经常涉及大量的同步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__block NSArray *databaseObjects;
__block NSArray *fileContents;
NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init];
NSBlockOperation *databaseOperation = [NSBlockOperation blockOperationWithBlock:^{
     databaseObjects = [databaseClient fetchObjectsMatchingPredicate:predicate];
}];
NSBlockOperation *filesOperation = [NSBlockOperation blockOperationWithBlock:^{
     NSMutableArray *filesInProgress = [NSMutableArray array];
     for  (NSString *path  in  files) {
         [filesInProgress addObject:[NSData dataWithContentsOfFile:path]];
     }
     fileContents = [filesInProgress copy];
}];
NSBlockOperation *finishOperation = [NSBlockOperation blockOperationWithBlock:^{
     [self finishProcessingDatabaseObjects:databaseObjects fileContents:fileContents];
     NSLog(@ "Done processing" );
}];
[finishOperation addDependency:databaseOperation];
[finishOperation addDependency:filesOperation];
[backgroundQueue addOperation:databaseOperation];
[backgroundQueue addOperation:filesOperation];
[backgroundQueue addOperation:finishOperation];

上面的代码能够简单地用合成signals来清理和优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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" );
     }];

简化集合转换

像map, filter, fold/reduce 这些高级功能在Foundation中是极度缺少的m导致了一些像下面这样循环集中的代码:

1
2
3
4
5
6
7
8
NSMutableArray *results = [NSMutableArray array];
for  (NSString *str  in  strings) {
     if  (str.length < 2) {
         continue ;
     }
     NSString *newString = [str stringByAppendingString:@ "foobar" ];
     [results addObject:newString];
}

RACSequence能够允许Cocoa集合用统一的方式操作:

1
2
3
4
5
6
7
RACSequence *results = [[strings.rac_sequence
     filter:^ BOOL (NSString *str) {
         return  str.length >= 2;
     }]
     map:^(NSString *str) {
         return  [str stringByAppendingString:@ "foobar" ];
     }];
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值