在CFRunloop中已经说明了一个线程及其runloop的对应关系 ,现在以iOS中NSThread的实际使用来说明runloop在线程中的意义。
美团CRM移动端团队技术博客
第一次遇到是在阅读AFNetworking中NSURLSession部分代码的时候:
1 | - (NSArray *)tasksForKeyPath:(NSString *)keyPath { __block NSArray *tasks = nil; dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) { if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks)) ]) { tasks = dataTasks; } else if ([keyPath isEqualToString:NSStringFromSelector(@selector( uploadTasks))]) { tasks = uploadTasks; } else if ([keyPath isEqualToString:NSStringFromSelector(@selector( downloadTasks))]) { tasks = downloadTasks; } else if ([keyPath isEqualToString:NSStringFromSelector(@selector( tasks))]) { //dataTasks, uploadTasks, downloadTasks 都是数组,相当于打平数组,返回一个包含子数组各元素的数组。 tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath: @"@unionOfArrays.self"]; } dispatch_semaphore_signal(semaphore); }]; dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); return tasks; } |
KVC集合运算符允许以key path符号在一个集合中执行方法,以@开头表示一个特定的集合方法。键值编码会在必要的时候把基本数据类型进行装箱或者拆箱
它主要分为3类:1.简单集合操作符 2.对象操作符 3.数组和集合操作符
在CFRunloop中已经说明了一个线程及其runloop的对应关系,现在以iOS中NSThread的实际使用来说明runloop在线程中的意义。
在iOS中直接使用NSThread有一下几种方式,但是归根到底,当一个线程需要长时间的去跟踪一个任务的时候,这几种方式做的事情是一样的,只不过接口名称和参数不一样,感觉是为了使用起来更加方便。因为这些接口内部都需要依赖runloop去实现事件的监听,这个可以通过调用堆栈证实。
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait
以上两个方法都是NSObject的方法,可以直接通过一个对象来创建一个线程。第二个方法具有更多的灵活性,它可以让你自己指定线程,第一个方法是自己默认创建一个线程。第二个方法的最后一个参数是指定是否等待aSelector执行完毕。
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;
该方法是NSThread的类方法,跟第一个方法是类似的功能。
下面通过在子线程发起一个网络请求,去发现一些问题,然后通过runloop去解释原因,并推测API背后的实现方式。
1 | - (void)viewDidLoad { [super viewDidLoad]; [self performSelectorInBackground:@selector(multiThread) withObject:nil]; } - (void)multiThread { if (![NSThread isMainThread]) { self.request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@" http://www.baidu.com"] cachePolicy:NSURLCacheStorageNotAllowed timeoutInterval:10]; [self.request setHTTPMethod: @"GET"]; self.connection =[[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:YES]; } } - (void)connection:(NSURLConnection *)connection didReceiveResponse:( NSURLResponse *)response{ NSLog(@"network callback"); } |
运行之后,可以发现在子线程中发起的网络请求,回调没有被调用。大致猜测可能跟runloop有关系,也就是子线程的runloop中没有注册网络回调的消息,所以该子线程自己相关的runloop没有收到回调。实际上- (instancetype)initWithRequest:(NSURLRequest *)request delegate:(id)delegate startImmediately:(BOOL) 这个方法的第三个参数的bool值表示是否在创建完NSURLConnection对象之后立刻发起请求,一般情况下是YES,什么时候会传NO呢。
事实上,对于以上这种方式创建的线程,默认是没有生成该线程对应的runloop的。也就是说这种情况下,需要自己去创建对应线程的runloop,并且让他run起来,去不断监听各种往runloop里注册的消息。但是对于主线程而言,其对应的runloop会由系统建立,并且自己run起来。由于平时工作在主线程下,这些工作大部分情况下不需要人为参与,所以一到子线程就会有各种问题。子线程中起timer没有生效也是相同的原因。所以以上函数第三个参数的意思就是,如果是当前线程已经runloop跑起来的情况下,传YES。除此之外,需要自己创建runloop去run,再将网络请求消息注册到runloop中。
现在根据以上分析修改代码:
1 | self.request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http:// www.baidu.com"] cachePolicy:NSURLCacheStorageNotAllowed timeoutInterval:10]; [self.request setHTTPMethod: @"GET"]; self.connection =[[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop run]; [self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode: NSDefaultRunLoopMode]; [self.connection start]; |
运行之后发现回调仍然没有被调用,其实在这里卡了很久。后来一次偶然的调试中发现,代码运行到 [runLoop run]; 就没有然后了。后面的代码一直就没有被执行,现在修改代码如下:
1 | self.request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http:// www.baidu.com"] cachePolicy:NSURLCacheStorageNotAllowed timeoutInterval:10]; [self.request setHTTPMethod: @"GET"]; self.connection =[[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode: NSDefaultRunLoopMode]; [self.connection start]; [runLoop run]; |
然后就发现网络回调被调用了。
之后分析了一下调用堆栈:
第一个:在multiThread里面是这样的:
第二个:网络回调里面是这样的:
http://7xqgnx.com1.z0.glb.clouddn.com/Runloop2.png
通过堆栈可以得知,这两个函数都是由线程6调用的,也就是创建的子线程,但是堆栈中的内容很不一样。很显然第二个是从runloop调出的,并且是Sources0这个消息调出的。而第一个是线程运行时候的初始化方法。所以当调用runlooprun的时候,其实是线程进入自己的runloop去监听时间了,从此以后,所有的代码都会从runloop CALLOUT出来。所以这种情况下,需要把先把消息注册到runloop中,让runloop跑起来是最后需要做的事情。
以下是开源库AFNetworking网络请求的实现:
1 | - (void)start { [self.lock lock]; if ([self isCancelled]) { [self performSelector:@selector(cancelConnection) onThread:[[self class ] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self. runLoopModes allObjects]]; } else if ([self isReady]) { self.state = AFOperationExecutingState; [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[ self.runLoopModes allObjects]]; } [self.lock unlock]; } + (void)networkRequestThreadEntryPoint:(id)__unused object { @autoreleasepool { [[NSThread currentThread] setName:@"AFNetworking"]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; } } + (NSThread *)networkRequestThread { static NSThread *_networkRequestThread = nil; static dispatch_once_t oncePredicate; dispatch_once(&oncePredicate, ^{ _networkRequestThread = [[NSThread alloc] initWithTarget:self selector: @selector(networkRequestThreadEntryPoint:) object:nil]; [_networkRequestThread start]; }); return _networkRequestThread; } |
AFNetworking使用的是- (void)performSelector:(SEL)aSelector onThread:(NSThread*)thr withObject:(id)arg waitUntilDone:(BOOL)wait这个方法,但是为什么它没有使用- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg这个方法呢?
通过断点,发现了AFNetwokring网络请求中一些函数的调用顺序:
1.networkRequestThread
2.networkRequestThreadEntryPoint
3.operationDidStart
为什么operationDidStart会在networkRequestThreadEntryPoint之后调用?
在networkRequestThreadEntryPoint里主要是生成网络线程的runloop并且让它跑起来,里面的 [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];这主要是为了在没有任何网络请求的时候让网络线程保持监听状态,否则网络线程的loop会直接返回,之后再调用网络线程请求就没有意义了。再结合调用堆栈,发现operationDidStart是在runloop callout出来的,而networkRequestThreadEntryPoint是网络线程的入口方法。这跟之前的例子是一样的。所以,我猜测- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait这个方法背后是由主线程将aSelector作为消息注册到runloop中时间发生在networkRequestThreadEntryPoint方法调用之前,所以在networkRequestThreadEntryPoint方法中调用,NSRunLoopcurrentRunLoop的时候其实runloop本身应该已经被创建了。原因是因为在这个地方断点 ,打印runloop对象可以发现里面已经注册了source0的消息,如下截图:
http://7xqgnx.com1.z0.glb.clouddn.com/Runloop3.png
也就是说父线程在- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait 函数中将aSelector注册成source0,这是该函数背后的大致实现。通过查阅apple官方文档,基本属实,如下所示:
http://7xqgnx.com1.z0.glb.clouddn.com/Runloop4.png
通过上面的分析,可以得出使用performSelector方法可以将子线程runloop的初始化实现在子线程的初始化方法里实现,如果使用performSelectorInBackground
方法,那么子线程runloop的初始化和业务逻辑就会混到一起,并且每一次都会重新初始化。AFNetworking通过一个静态全局的子线程去管理所有的网络请求,其对应的runloop也只需要初始化一次。
通过以上分析,可以知道如果需要让一个子线程去持续的监听时间,就需要启动它的runloop并且忘其中注册source,timer,oberserver三者之一的消息类型。在默认情况下子线程的runloop是不会自己创建和启动的。
理解:个人觉得是对RACSignal的封装,侧重于对事件的信号的封装。有了RACComand,就可以把动作通过它很方便的放到ViewModel中,类似于以下这种方式:
1.在ViewModel中定义RACCommand
1 | @interface SubscribeViewModel : NSObject @property(nonatomic, strong) RACCommand *subscribeCommand; @end |
2.在ViewModel中处理具体的RACCommand封装的动作信号
1 | - (RACCommand *)subscribeCommand { if (!_subscribeCommand) { NSString *email = self.email; _subscribeCommand = [[RACCommand alloc] initWithEnabled:self. emailValidSignal signalBlock:^RACSignal *(id input) { return [SubscribeViewModel postEmail:email]; }]; } return _subscribeCommand; } |
input参数是在command调用excute方法的时候传进来的。
3.在ViewController中将ViewModel的RACCommand绑定到控件的事件信号上
1 | self.subscribeButton.rac_command = self.viewModel.subscribeCommand; |
如果要对某个行为进行单元测试也非常方便,直接对ViewModel层进行测试就可以了,并且没有任务Uiew层的显示逻辑,纯业务逻辑测起来很方便。如果没有RACCommand,直接通过signal去处理,可能类似这种方式:
1 | [[self.button rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) { }]; |
直接在ViewController中定义了动作的逻辑,没有很好的分离。
1 | _subscribeCommand = [[RACCommand alloc] initWithEnabled:self.emailValidSignal signalBlock:^RACSignal *(id input) { return [SubscribeViewModel postEmail:email]; }]; |
signalBlock必须要返回一个信号,不能传nil,如果不想传递信号,则可以创建空的信号
1 | [RACSignal empty] |
RAC
emailValidSignal用来指定创建的命令是否能够执行,self.emailValidSignal定义如下:
1 | _emailValidSignal = [RACObserve(self, email) map:^id(NSString *email) { return @([email isValidEmail]); }]; |
当self.emailValidSignal信号返回数据为yes的时候,_subscribeCommand才能够响应行为
signalBlock是当_subscribeCommand执行的时候会被调用的逻辑,该block返回的是一个包含了命令执行之后结果的signal。
allowsConcurrentExecution代表当该命令正在执行的时候,是否能够再次触发改命令。
RACCommand内部是如何通过它去控制当前命令能不能够被执行以及能不能并发的执行:
以UIButton举例:
UIButton+RACCommandSupport.m
1 | //UIButton的enabel和command的enabel绑定起来了,也就是如果command的enabel //设置未false,那么button则不可用。 disposable = [command.enabled setKeyPath:@keypath(self.enabled) onObject:self]; |
以下是RACCommand中有关allowsConcurrentExecution的源码:
1 | if (enabledSignal == nil) { enabledSignal = [RACSignal return:@YES]; } else { enabledSignal = [[[enabledSignal startWith:@YES] takeUntil:self.rac_willDeallocSignal] replayLast]; } _immediateEnabled = [[RACSignal combineLatest:@[ enabledSignal, moreExecutionsAllowed ]] and]; _enabled = [[[[[self.immediateEnabled take:1] concat:[[self.immediateEnabled skip:1] deliverOn:RACScheduler. mainThreadScheduler]] distinctUntilChanged] replayLast] setNameWithFormat:@"%@ -enabled", self]; |
整个command是否可用,是由immeditateEnable来决定的,而imediateEnabled是取enableSignal和moreExecutionsAllowed的与操作。
enableSignal是command初始化方法传进来的第一个参数。
1 | RACSignal *immediateExecuting = [RACObserve(self, activeExecutionSignals) map:^ (NSArray *activeSignals) { return @(activeSignals.count > 0); }]; RACSignal *moreExecutionsAllowed = [RACSignal if:RACObserve(self, allowsConcurrentExecution) then:[RACSignal return:@YES] else:[immediateExecuting not]]; |
imediateExecuting是当前正在执行未完成的命令数量的信号,如果command的allowsConcurrentExecution设置的yes,则moreExcutionsAllowed返回yes,反之则取决于有没有正在执行的信号,如果有就返回false,没有返回yes。一个信号是否执行完,根据signal有没有发出complete事件来判断的。
1 | RACSignal *signal =[command execute:@2]; |
在command的初始化方法中,有一个signalBlock参数,该block有一个input参数,在command执行excute方法的时候传入的参数被当作signalBlock的input参数传到signalBlock中。比如通常将一个网络请求的封装成一个Command,此时网络请求的参数就可以通过调用excute的时候传入。
这种情况下,获取command执行之后返回的数据可以通过:
1 | RACSignal *signal =[command execute:@2]; // 在这里就可以订阅信号了 [signal subscribeNext:^(id x) { NSLog(@"%@",x); }]; |
订阅命令完成的信号
1 | [[self.viewModel.subscribeCommand execute:nil] subscribeCompleted:^{ NSLog(@"The command executed"); }]; |
属于信号中的信号,在subscribeNext中返回的是信号x,再次订阅信号x才能获取具体数据
1 | [command.executionSignals subscribeNext:^(RACSignal *x) { [x subscribeNext:^(id x) { NSLog(@"%@", x); }]; |
也可以直接使用excutionSignals.switchToLatest subscribeNext:获取到数据
1 | // 监听登录产生的数据 [_LoginCommand.executionSignals.switchToLatest subscribeNext:^(id x) { if ([x isEqualToString:@"登录成功"]) { NSLog(@"登录成功"); } }]; |
现在iOS页面布局用的最多的就是Frame和Autolayout,在Autolayout通过Masonry封装在实际使用中也十分方便。实际上,Autolayout的约束最后都是系统最终转化成frame来进行布局的,对与一个View来说,最终确定其中心点位置和View的宽高。当Autolayout和Frame设置上产生冲突的时候,则会以Autolayout的设置为准。这篇主要讨论布局中常用的几个方法和autolayout遇到动画的情形。
RAC中统一的数据接口,控件的事件,包括KVO,timer都可以转化成RACSignal。
创建:
1.RAC未控件的一部分原来的事件都通过Category的方式定义了event对应的signal,只需要直接拿来使用就好了。
2.自己创建
1 | + (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber)) didSubscribe; |
3.订阅
1 | [self.usernameTextField.rac_textSignal subscribeNext:^(id x) { NSLog(@"%@", x); }]; |
在信号创建的时候,需要传入一个didSubscribe的block,在有其他订阅者订阅这个信号的时候,didSubscribe就会被调用,然后将数据通过subscribeNext的block传入。
1.next,一般情况下,信号处理业务逻辑正常返回的时候,会调用订阅者的sendNext方法将数据传入订阅者,订阅者可以通过过subscribeNext获取数据。
2.error,有时候业务逻辑产生异常的时候,会调用订阅者的sendError方法来告知订阅者产生了异常,订阅者在subscribeNext:erro:中处理异常错误。
3.completed,该事件表示订阅者从信号中移除,之后不再收到消息了,信号生命周期结束。
创建一个请求账号权限的信号
1 | - (RACSignal *)requestAccessToTwitterSignal { // 1 - define an error NSError *accessError = [NSError errorWithDomain:RWTwitterInstantDomain code: RWTwitterInstantErrorAccessDenied userInfo:nil]; // 2 - create the signal @weakify(self) return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { // 3 - request access to twitter @strongify(self) [self.accountStore requestAccessToAccountsWithType:self.twitterAccountType options:nil completion:^(BOOL granted, NSError *error) { // 4 - handle the response if (!granted) { [subscriber sendError:accessError]; } else { [subscriber sendNext:nil]; [subscriber sendCompleted]; } }]; return nil; }]; } |
处理请求账号权限的信号
1 | [[self requestAccessToTwitterSignal] subscribeNext:^(id x) { NSLog(@"Access granted"); } error:^(NSError *error) { NSLog(@"An error occurred: %@", error); }]; |
将信号过滤,只保留满足block中条件的信号
只保留输入字符串长度大于3的信号
1 | [[self.usernameTextField.rac_textSignal filter:^BOOL(id value) { NSString *text = value; return text.length > 3; }] subscribeNext:^(id x) { //这里只会显示长度大于3的字符串 NSLog(@"%@", x); }]; |
这里的信号先经过filter过滤了一次,再由subscribeNext得到过滤的信号进行处理,其数据流如下图所示:
把源信号的值映射成一个新的值
1 | [[[self.usernameTextField.rac_textSignal map:^id(NSString *text) { return @(text.length); }] filter:^BOOL(NSNumber *length) { return [length integerValue] > 3; }] subscribeNext:^(id x) { NSLog(@"%@", x); }]; |
信号先经过map映射将字符串转化成功了字符串长度的数据,再通过filter过滤掉了字符串长度等于3的信号,最后通过subscribeNext获得最后符合条件的信号,整个过程包含了不同数据类型的变化,过程如下图:
http://7xqgnx.com1.z0.glb.clouddn.com/RAC2.png
将多个信号合并起来,每一个被合并的信号必须调用过一次sendNext,才能触发合并的信号,最后以元组的形式发出。
当信号发出的内容是元组时,可以使用它将元组聚合成一个值。
将对姓名和密码的验证结果的两种信合合并成一个信号,再通过reduce返回最终是否验证通过的信号。
1 | RACSignal *signUpActiveSignal = [RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal] reduce:^id(NSNumber *usernameValid, NSNumber *passwordValid ) { return @([usernameValid boolValue] && [passwordValid boolValue]); }]; |
数据流的状态:
在每次信号执行订阅者的Next方法之前,会调用这个方法,它对信号本身不会产生影响。
在按钮点击之后,会有一系列验证的过程,在这期间,按钮不能被再次点击,等待验证结果出来之后,在恢复按钮未正常使用的状态。
1 | [[[[self.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside] doNext:^(id x) { self.signInButton.enabled = NO; self.signInFailureText.hidden = YES; }] flattenMap:^id(id x) { return [self signInSignal]; }] subscribeNext:^(NSNumber *signedIn) { self.signInButton.enabled = YES; BOOL success = [signedIn boolValue]; self.signInFailureText.hidden = success; if (success) { [self performSegueWithIdentifier:@"signInSuccess" sender:self]; } }]; |
数据流的状态:
用于连接两个信号,当第一个信号完成之后(前一个信号调用sendCompleted),才会连接then返回的信号,then之前的信号会被忽略
在得到否有权限获得账户信息之后,通过then获取搜索文本的信号,再验证搜索文本的有效性,最后获取有效的搜索文本。如果获取账号信息这一步出现错误,直接调用最后的error的block。
1 | [[[[self requestAccessToTwitterSignal] then:^RACSignal *{ @strongify(self) return self.searchText.rac_textSignal; }] filter:^BOOL(NSString *text) { @strongify(self) return [self isValidSearchText:text]; }] subscribeNext:^(id x) { NSLog(@"%@", x); } error:^(NSError *error) { NSLog(@"An error occurred: %@", error); }]; |
数据流如下图:
信号传递切换到指定线程中
现在创建一个信号在后台线程下载一个图片,可以考虑使用SDWebImage
1 | -(RACSignal *)signalForLoadingImage:(NSString *)imageUrl { RACScheduler *scheduler = [RACScheduler schedulerWithPriority:RACSchedulerPriorityBackground]; return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber ) { NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl] ]; UIImage *image = [UIImage imageWithData:data]; [subscriber sendNext:image]; [subscriber sendCompleted]; return nil; }] subscribeOn:scheduler]; } |
图片下载完成之后切换到主线程,设置图片的显示
1 | cell.twitterAvatarView.image = nil; [[[self signalForLoadingImage:tweet.profileImageUrl] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(UIImage *image) { cell.twitterAvatarView.image = image; }]; |
节流,可以设置某一段时间内不发送信号,等过了这段时间,将最新的信号发出
在用户输入搜索文字的时候,每一次textChange都会触发搜索结果的网络请求。请求次数过于频繁且没有必要,因为有时候可能是用户还没有输完而已。并且结果频繁的显示清空体验也不好。现在通过throttle处理每经过0.5秒处理一次搜索请求。
1 | [[[[[[[self requestAccessToTwitterSignal] then:^RACSignal *{ @strongify(self) return self.searchText.rac_textSignal; }] filter:^BOOL(NSString *text) { @strongify(self) return [self isValidSearchText:text]; }] throttle:0.5] flattenMap:^RACStream *(NSString *text) { @strongify(self) return [self signalForSearchWithText:text]; }] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDictionary *jsonSearchResult) { NSArray *statuses = jsonSearchResult[@"statuses"]; NSArray *tweets = [statuses linq_select:^id(id tweet) { return [RWTweet tweetWithStatus:tweet]; }]; [self.resultsViewController displayTweets:tweets]; } error:^(NSError *error) { NSLog(@"An error occurred: %@", error); }]; |
经过最后的处理,数据流如下图所示:
在requestAccessToTwitterSignal和signalForSearchWithText都有可能产生error,最后都会直接调用subscribleNext:error方法。
(RACSignal *) 获取信号直到某个信号执行完成
比如,监听某个文本框的文本改变直到当前对象被销毁
1 | [_textField.rac_textSignal takeUntil:self.rac_willDeallocSignal]; |
(NSUInteger) 跳过几个信号,选择忽略不处理
第一次输入不被监听
1 | [[_textField.rac_textSignal skip:1] subscribeNext:^(id x) { NSLog(@"%@",x); }]; |
1 | RAC(_titleLabel, text) = [viewModel.titleSignal takeUntil:self. rac_prepareForReuseSignal]; |
表示将一个对象的属性和一个signal进行绑定,signal每产生一个value,都会自动执行如下代码
1 | [TARGET setValue:value ?: NIL_VALUE forKeyPath:KEYPATH]; |
返回一个signal,检测target的keypath属性
RAC和RACObserve用在一起实现双向绑定
1 | RAC(self.outputLabel, text) = RACObserve(self.model, name); |