ReactiveObjC官方文档翻译

前言

此为ReactiveObjC文档翻译,gitHub tag:2.12 date:2017-03-15 QQ: 809199658.
原文地址:ReactiveCocoa/ReactiveObjC(四级险过翻译,请谅解)

ReactiveObjC

提示:本文是使用老版本ReactiveCocoa - Objective-C的介绍,Objective-C ReactiveCocoa又名ReactiveObjC,如果希望使用新版(swift)的ReactiveCocoa,可以查看ReactiveCocoa或者ReactiveSwift.

ReactiveObjC (一般来说又叫做ReactiveCocoa或者RAC)是一个基于响应式编程的Objective-C的框架.使用它提供API用以创建和改变数据流.

如果你已经熟悉响应式编程或者了解基本的ReactiveObjC概念,你可以查看github上的其他文件来了解其框架的概述然后在实践中深入了解其使用

新接触ReactiveObjC?

ReactiveObjC的文档非常完善,并且有非常丰富的介绍材料用以了解并使用RAC.

如果你想学习更多,我们推荐你下列的这些资源

  1. introduction
  2. when-to-use-reactiveobjc
  3. Framework Overview
  4. Basic Operators
  5. Header Documentation
  6. Stack Overflow之前问题的回答或者GitHub issues.
  7. 本文章的剩余部分
  8. Functional Reactive Programming on iOS (eBook)

如果你还有更多问题,请提交issue

介绍

ReactiveObjC是基于函数响应式编程,相对于修改或替换现有变量,RAC提供信号类(代表:RACSugnal)来捕捉现有和 未来(future???)的变量

通过链接,绑定以及响应信号,不需要监听来更新变量,使得程序更加简洁

例如,一个文本框被绑定后,相对于使用额外的代码去监听时间和更新文本框,使用block来重写RAC使得其更像KVO
-observeValueForKeyPath:ofObject:change:context:

信号类也可以像多线程一样使用,像并发式编程.这简化了包括在网络请求在内的多线程程序.

RAC的最大优点就是它提供了一个信号,统一了解决异步行为的方法,包括代理方法,target-action机制,通知,和KVO.

例1:

// 当self.username 改变时,控制台会输出他的新名字
//
// RACObserve(self, username)在文本改变的时候创建了发送当前姓名的一个新信号
// -subscribeNext: 在收到信号后执行block
[RACObserve(self, username) subscribeNext:^(NSString *newName) {
    NSLog(@"%@", newName);
}];

相对于KVO,信号可以被链接到一起并一同执行

//只打印以'j'开头的名字
//
// 过滤器在第一个信号block被调用返回YES时才发送一个新值给下一个block
[[RACObserve(self, username)
    filter:^(NSString *newName) {
        return [newName hasPrefix:@"j"];
    }]
    subscribeNext:^(NSString *newName) {
        NSLog(@"%@", newName);
    }];

信号也可以用来获取状态,RAC可以将属性快速的转换成信号和操作来替代监听属性然后改变值得这种方式.

//创建一个单程绑定,这样当self.password和self.passwordConfirmation一样时self.createEnabled将会变成ture
//
// RAC() 宏可以使得绑定表示的更清楚
//
// +combineLatest:reduce: 获取一个信号数组,当其中任何一个改变时传递新值并调用这些blocks,并且返回block返回值的一个新RACSignal信号
//(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]);
    }];

信号可以在任何时间任何数据流上创建,不仅限于KVO,例如,可以用于按钮点击事件

// 在按钮点击时打印信息
// 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) {
    // 这个登录命令在网络请求结束后返回发送值得一个信号
    return [client logIn];
}];

// -executionSignals 返回一个包含上面block的信号,收到一次信号执行一次
[self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {
    // 登录成功调用
    [loginSignal subscribeCompleted:^{
        NSLog(@"Logged in successfully!");
    }];
}];

// 按钮点击时执行登录命令
self.loginButton.rac_command = self.loginCommand;

信号也可以在timer,UI事件,或者其他随时间改变的事情上

将多个信号链接在一起成为一组操作(a group of operations completes)可以使得更复杂的异步操作变得简单

// 在两个网络请求结束后打印信息
//
// +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: 在信号结束后调用
[[RACSignal
    merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]]
    subscribeCompleted:^{
        NSLog(@"They're both done!");
    }];

多个异步操作也可以将信号链接实现,不同于回调block,这个和并发操作的一般做法一致.

// 登录用户,服务器获取信息,获取本地缓存信息,之后显示全部信息
//
// hypothetical -logInUser 在登录完成后返回完成信号
//
// -flattenMap: 在信号发送后执行block, 返回一个合并所有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
//
//hypothetical -fetchUserWithUsername: 发送给用户的信号
//
// -deliverOn: 在其他队列上创建一个新的信号在本例中,
//它用来将任务放置在后台队列然后返回主队列中
//
RAC(self.imageView, image) = [[[[client
    fetchUserWithUsername:@"joshaber"]
    deliverOn:[RACScheduler scheduler]]
    map:^(User *user) {
        // 在后台下载头像
        return [[NSImage alloc] initWithContentsOfURL:user.avatarURL];
    }]
    // 此操作在主线程
    deliverOn:RACScheduler.mainThreadScheduler];

上面这些例子表明了RAC可以做的事情,但是很难再README大小的例子中展示为什么RAC很犀利,但是可以表明RAC怎么样让代码更清晰(吹牛中)....

如果要更多的代码示例,可以查看C-41或者GroceryList.其中所有的iOS APP都是用RAC来实现的,更多关于RAC的信息都可以在其中找到.

什么时候使用RAC

初看的时候,RAC非常抽象,而且很难了解什么时候使用来解决问题.
接下来的demo是RAC擅长的领域.

操作异步或者事件驱动数据源

许多的cocoa程序关注于程序对事件的响应或改变,处理的代码很可能变得非常复杂(就像意大利面条🍝一样),非常多的回调block和状态变量用来处理问题.
样式(Patterns)看起来是突出的那个,像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);
//将button的enable和accountText,passwordText,isLogin,logined绑定起来
    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);
        }];
//loginbutton 的点击事件
    [[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];
}];

使用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.");
    }];

独立并行功能

在并行的线程处理独立数据并把处理好的数据合并在一起在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];

使用RAC可以简化成如下代码

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也可能在fondation框架中缺失或者导致loop-focused崩溃.

//在遍历中添加数据导致无限循环
NSMutableArray *results = [NSMutableArray array];
for (NSString *str in strings) {
    if (str.length < 2) {
        continue;
    }

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

RAC中RACSequence允许在遍历中添加值.

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

系统要求

OS X 10.8+ 及iOS8.0+

导入ReactiveObjC

导入ReactiveObjC你可以

  1. 添加ReactiveObjC仓库到你的程序仓库中
  2. 在你的ReactiveObjC文件夹中运行
    git submodule update --init --recursive
  3. ReactiveObjC.xcodeproj拖入你的Xcode项目中
  4. 在"Build Phases"中添加RAC到"Link Binary With Libraries"中
  5. 添加ReactiveObjC.frameworkRAC也必须添加到任何"Copy Frameworks"build phase中.如果你还没创建,简单的添加一个"Copy Files" build phase指向(target)"Frameworks"位置
  6. 添加"$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/include" $(inherited)到"Header Search Paths" build setting 中
  7. 如果是iOS,则需要在"Other Linker Flags" build setting添加-ObjC

后记

四级渣翻译,如果有什么意见或者改善的地方可以留言或者通过QQ联系我~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容