ReactiveObjC 3.1.1

ReactiveObjC 3.1.1

Tests已测试
Lang语言 Obj-CObjective C
许可证 NOASSERTION
Released上一个版本2019年4月

Ash FurrowSyo IkedaEric HoracekAnders Ha 维护。




ReactiveObjC

注意:这是 Objective-C ReactiveCocoa 的旧版介绍,现在已知为 ReactiveObjC。有关使用 Swift 的更新版本,请参阅 ReactiveCocoaReactiveSwift

ReactiveObjC(先前称为 ReactiveCocoa 或 RAC)是一个受功能反应式编程启发的 Objective-C 框架。它提供用于 组合和转换值流 的 API。

如果您已经熟悉功能反应式编程或了解 ReactiveObjC 的基本前提,请参阅此文件夹中的其他文档,了解框架概述以及关于如何在实践中操作的更多详细信息。

首次接触 ReactiveObjC?

ReactiveObjC 的文档非常丰富,有大量的入门材料可以解释 RAC 是什么以及如何使用它。

如果您想了解更多的信息,我们推荐以下资源,按顺序大致排列:

  1. 简介
  2. 何时使用 ReactiveObjC
  3. 框架概述
  4. 基本操作符
  5. 头文件文档
  6. 先前回答的 Stack Overflow 问题以及 GitHub 问题
  7. 此文件夹的其余部分
  8. iOS 上的功能反应式编程(电子书)

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

介绍

ReactiveObjC 受到函数式响应式编程的启发。与使用替换和就地修改的可变变量不同,RAC 提供了捕获当前和未来值的信号(用 RACSignal 表示)。

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

例如,一个文本字段可以绑定到最新时间,而不是使用额外的代码来监视时钟,每隔一秒更新文本字段。它的工作方式类似于 KVO,但使用块而不是重写 -observeValueForKeyPath:ofObject:change:context:

信号也可以表示异步操作,类似于未来和承诺。这大大简化了异步软件,包括网络代码。

RAC 的一个重要优势是它提供了一种单一、统一的方法来处理异步行为,包括委托方法、回调块、目标操作机制、通知和 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编程的大部分内容都集中于对用户事件或应用程序状态变化的反应。处理此类事件的代码很快就会变得非常复杂并呈 spaghetti 式,有大量的回调和状态变量来处理排序问题。

表面上看似不同的模式,如 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];

以上代码可以通过简单地组合信号进行清理和优化。

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相关的资源