RACObjC 3.3.0

RACObjC 3.3.0

Dmitry Lizin 维护。



RACObjC 3.3.0

  • 作者
  • Dmitry Lizin

ReactiveObjC

ReactiveObjC(之前称为 ReactiveCocoa 或 RAC)是一个受 Functional Reactive Programming 启发的 Objective-C 框架。它提供了用于 组合和转换值流 的 API。

如果你已经熟悉函数式反应式编程,或者了解 ReactiveObjC 的基本概念,请检查此文件夹中的其他文档,以了解框架概述以及在实践中的应用。

你是 ReactiveObjC 的初学者吗?

ReactiveObjC 文档详尽,有许多入门材料可以解释 RAC 是什么以及如何使用它。

如果你想了解更多的信息,我们建议以下资源,大致按顺序排列

  1. 简介
  2. 什么时候使用 ReactiveObjC
  3. 框架概述
  4. 基本算子
  5. 头文件文档
  6. 之前回答的 Stack Overflow 问题以及 GitHub 问题
  7. 本文件夹的其余部分
  8. iOS 上的函数式反应式编程(电子书)

如果您有任何进一步的问题,请随时 提交问题

简介

ReactiveObjC 受函数式反应式编程的启发。它不使用可变变量,这些变量可以被替换和就地修改,而是提供信号(由 RACSignal 表示),以捕获当前和未来的值。

通过链接、组合和对信号做出反应,软件可以声明性地编写,而无需编写不断观察和更新值的代码。

例如,一个文本框可以绑定到最快的时间,甚至在时间改变时,而不需要额外的代码来监视时钟,每秒钟更新一次文本框。它的工作方式类似于 KVO,但它使用块来代替重写 -observeValueForKeyPath:ofObject:change:context:

信号也可以表示异步操作,就像 futures 和 promises。这极大地简化了异步软件,包括网络代码。

ReactiveObjC 的一个主要优点是它提供了一种单一的、统一的方法来处理异步行为,包括委托方法、回调块、目标-动作机制、通知和 KVO。

这里有一个简单的例子

// 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 通知不同,信号可以链接在一起并对其进行操作

// 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);
	}];

信号也可以用来导出状态。而不是观察属性并在新值响应时设置其他属性,RAC 使得以信号和操作的形式表达属性成为可能

// 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]);
	}];

信号可以建立在任何随时间流的值流上,而不仅仅是 KVO。例如,它们也可以代表按钮点击

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

或者异步网络操作

// 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;

信号也可以表示计时器、其他 UI 事件或任何其他随时间变化的东西。

使用信号进行异步操作可以通过链接和转换这些信号来构建更复杂的行为。可以在一系列操作完成后轻松触发工作

// 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!");
	}];

信号可以被链式调用,以便顺序执行异步操作,而不是在回调中使用嵌套代码块。这与通常使用的期货和承诺相似

// 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甚至能简化绑定异步操作的结果

// 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,但它使得编写具有更少状态、更少样板代码、更好的代码局部性和更明确意图表达的代码成为可能。

更多示例代码请参见C-41GroceryList,它们是使用ReactiveObjC编写的真实iOS应用。关于RAC的更多信息,请查看此文件夹。

什么时候使用 ReactiveObjC

乍一看,ReactiveObjC非常抽象,并且理解如何将其应用于具体问题可能很困难。

以下是RAC擅长的一些用例。

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

大多数Cocoa编程都集中在响应用户事件或应用程序状态的改变上。处理此类事件的代码可能会迅速变得非常复杂和像意大利面一样,需要处理大量的回调和状态变量以处理顺序问题。

表面上看似不同的模式,如UI回调、网络响应和KVO通知,实际上有很多共同之处。《a href = "ReactiveOjC.RACSignal.h"> 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];

上述代码可以通过简单组合信号来清理和优化

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

简化集合转换

在Foundation中严重缺少像mapfilterfold/reduce等高级函数,导致像这样以循环为焦点的代码。

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

系统要求

ReactiveObjC支持OS X 10.8+和iOS 8.0+

导入ReactiveObjC

要将RAC添加到您的应用程序中

  1. 将ReactiveObjC仓库作为应用程序仓库的子模块添加。
  2. 在ReactiveObjC文件夹中运行git submodule update --init --recursive
  3. ReactiveObjC.xcodeproj拖放到您的应用程序Xcode项目或工作空间中。
  4. 在您的应用程序目标的"构建阶段"选项卡上,将RAC添加到"与二进制链接库"阶段。
  5. 添加ReactiveObjC.framework。RAC还必须添加到任何"复制框架"构建阶段。如果您尚未添加,请简单地添加一个"复制文件"构建阶段,并将目标设置为"框架"。
  6. "$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/include" $(inherited)添加到"标题搜索路径"构建设置(这只适用于归档构建,但没有负面影响)。
  7. 对于iOS目标,请将-ObjC添加到"其他链接器标志"构建设置中。
  8. 如果您将RAC添加到一个项目(不是工作空间),您还需要将适当的RAC目标添加到应用程序的"目标依赖项"中。

要查看已经配置了RAC的项目,请查看 C-41GroceryList,这些是使用ReactiveObjC编写的真实iOS应用。

更多信息

ReactiveObjC受到了.NET的Reactive Extensions (Rx)的启发。Rx的多数原则也适用于RAC。有许多优秀的Rx资源。

RAC和Rx都是受函数式响应式编程启发的框架。以下是与FRP相关的一些资源。