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