iOS ReactiveObjC使用

作者: 乌龟漫漫 | 来源:发表于2022-06-21 10:28 被阅读0次

介绍

ReactiveObjC的灵感来自函数式响应式编程。RAC使用了捕获当前值和未来值的信号(RACSignal),而没有使用替换和修改变量值的方式。

通过链接、组合和对信号响应的方式以声明的方式编写程序。

例如,一个输入框可以获取最新的时间,即使发什了改变,也不用使用额外的代码来监听时间和每秒更新文本内容。它的工作原理很像KVO,但是是使用了block代替-observeValueForKeyPath:ofObject:change:context:方法。

信号也可以用来实现异步操作,就像futurespromises一样。这大大简化了异步操作,包括网络请求相关代码。

RAC的一个主要优点是它提供了一个单一的、统一的方法来处理异步行为,包括delegateblockstarget-action机制、notificationsKVO

简单例子:

// 当 self.username 改变, 打印最新的值
//
// RACObserve(self, username) 创建了一个新的信号量用来发送信号
// selg.username每当改变时就会有一个新值。
// 将在信号发送一个新值时执行subscribeNext相关block方法
 [RACObserve(self, username) subscribeNext:^(NSString *newName) {
    NSLog(@"%@", newName);
}];

但是和KVO通知不同的是,Signals可以组合并共同操作:

//只打印以“j”开头的名字
//
//-filter返回一个新的RACSignal,当它的内容返回YES时。
[[RACObserve(self, username)
    filter:^(NSString *newName) {
        return [newName hasPrefix:@"j"];
    }]
    subscribeNext:^(NSString *newName) {
        NSLog(@"%@", newName);
    }];

Signals也可以用来获取状态。RAC不需要观察属性并根据新值设置其他属性,而是可以用信号和操作来表示属性:

//创建一个单向绑定用判断self.createEnabled和self.password的值是否相等,若相等则self.createEnabled等于true
//RAC()是一个很好的绑定宏
//
//+combineLatest接收一个数组,用来表示当任何信号发送变化时,获取每个信号的最新值并返回一个信号量并将最新值返回。
RAC(self, createEnabled) = [RACSignal
    combineLatest:@[ RACObserve(self, password), RACObserve(self, passwordConfirmation) ]
    reduce:^(NSString *password, NSString *passwordConfirm) {
        return @([passwordConfirm isEqualToString:password]);
    }];

不止KVOSignals可以随着时间改变建立在任何stream值上。例如,它可以用来表示按下按钮:

// 当按下按钮时记录信息
//
// RACCommand 创建一个信号来表示UI操作。例如,每个信号可以表示按下按钮及与之相关的操作。
//
// -rac_command 是对NSButton的补充。每当按钮被按下时他就会发送命令给自己。
self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
    NSLog(@"button was pressed!");
    return [RACSignal empty];
}];

或者异步网络操作:

//点击登录按钮是进行网络请求
//
//当执行登录操作时,这个block将被运行,程序执行登录操作
self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) {
    // 假设-login方法返回一个信号,当网络请求完成时返回一个值。
    return [client logIn];
}];
 
//-executionSignals返回一个信号,该信号每当执行一次都有一个包含上述返回的信号
[self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {
    // 当登录成功时打印信息
    [loginSignal subscribeCompleted:^{
        NSLog(@"Logged in successfully!");
    }];
}];
 
//按下登录按钮执行登录操作
self.loginButton.rac_command = self.loginCommand;

Signals还可以用作定时器,其他UI事件,或者任何随着时间变化的东西。

在异步操作中可以通过连接和转换这些信号来构建更为复杂的操作。在操作完成后可以轻松的触发:

//执行2个网络请求,完成后进行打印操作
//
// +merger:接收一个信号数组并返回一个新的RACSignal,该RACSignal传递所有信号的值,并在所有信号完成后进行完成。
//
//-subscribeCompleted:将在信号完成时执行
[[RACSignal
    merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]]
    subscribeCompleted:^{
        NSLog(@"They're both done!");
    }];

Signals可以链接起来以顺序执行的方式进行异步操作,而不是用block嵌套的方式回调。这类似于futurespromise的用法:

//记录用户,然后加载所有缓存的消息,然后从服务器获取剩余的消息。完成所有操作后,将一条消息记录到控制台。
//
//假设-logInUser方法返回一个在登录完成后的信号。
//
//-flattenMap:每当有一个信号发送值是,将执行该block代码,并返回一个将所有返回信号合并成单一信号的RACSignal。
[[[[client
    logInUser]
    flattenMap:^(User *user) {
        // 返回一个用户加载缓存消息的信号。
        return [client loadCachedMessagesForUser:user];
    }]
    flattenMap:^(NSArray *messages) {
        // 返回一个获取任何剩余消息的信号。
        return [client fetchMessagesAfterMessage:messages.lastObject];
    }]
    subscribeNext:^(NSArray *newMessages) {
        NSLog(@"New messages: %@", newMessages);
    } completed:^{
        NSLog(@"Fetched all messages.");
    }];

RAC也可以很容易绑定异步操作的结果:

//创建一个self.imageView.image一单被下载就会被设置成为用户头像的单向绑定。
//
//假设-fetchUserWithUsername:方法返回一个发送给用户的信号。
//
//-deliverOn:创建一个在其他队列上工作的新信号。在本例中,它用于将工作转移到后台队列,然后返回到主线程。
//
//-map:对每个调用block的用户,返回一个新的RACSignal,发送block返回的值。
RAC(self.imageView, image) = [[[[client
    fetchUserWithUsername:@"joshaber"]
    deliverOn:[RACScheduler scheduler]]
    map:^(User *user) {
        // 下载角色(这是在后台队列中完成的)。
        return [[NSImage alloc] initWithContentsOfURL:user.avatarURL];
    }]
    // 赋值将在主线程完成
    deliverOn:RACScheduler.mainThreadScheduler];

何时使用ReactiveObjc

初次接触,ReactiveObjc非常的抽象,很难想象如何将其运用于具体的问题。
以下是RAC擅长解决问题的一些用例。

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

大部分Cocoa编程都专注于响应用户事件或应用程序状态的改变。处理这类事件的代码可能会变得非常复杂,就像意大利面一样,要用很多回调和状态变量来处理顺序问题。

表面上看起来不同的模式,如UI回调、网络响应和KVO通知,实际上有很多相同之处。RACSignal统一了所以这些不同的api,以便它们可以组合在一起以相同的方式处理问题。

例如,以下代码:

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:UserDidLogOutNotification object:nil]
        mapReplace:@NO];
}

链接回调操作

依赖关系最常见于网络请求中,即前一个请求完成才能进行下一个请求,以此类推:

[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];
}];

ReactiveObjc让这中模式变得特别容易:

[[[[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中非常常见,通常涉及到大量的同步操作:

__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通过简单的组合来优化:

RACSignal *databaseSignal = [[databaseClient
    fetchObjectsMatchingPredicate:predicate]
    subscribeOn:[RACScheduler scheduler]];
 
RACSignal *fileSignal = [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id<RACSubscriber> 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中严重缺失,导致只能使用循环语句:

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

RACSequence允许以统一且声明的方式操作任何Cocoa集合:

RACSequence *results = [[strings.rac_sequence
    filter:^ BOOL (NSString *str) {
        return str.length >= 2;
    }]
    map:^(NSString *str) {
        return [str stringByAppendingString:@"foobar"];
    }];

注:以上内容翻译于github官方文档,如有错误,欢迎指正。

相关文章

网友评论

    本文标题:iOS ReactiveObjC使用

    本文链接:https://www.haomeiwen.com/subject/fogsvrtx.html