際際滷

際際滷Share a Scribd company logo
ReactiveCocoa
in Practice
Jeames Bone Mark Corbyn
@outware
www.outware.com.au
@jeamesbone @markcorbyn
ReactiveCocoa
What is ReactiveCocoa?
Functional Reactive Programming (FRP) framework
for iOS and OSX applications.
Why ReactiveCocoa?
Instead of telling a computer how to do its job, why dont we just tell it
what its job is, and let it figure the rest out.
ReactiveCocoa in a Nutshell
The main component in ReactiveCocoa is the Signal.
Signals represent streams of values over time.
1 3 2 T
Signals
h he hel T
[textField.rac_textSignal subscribeNext:^(NSString *text) {
NSLog(@"text: %@", text);
}];
> text: h
> text: he
> text: hel
Operators
1 3 2
[signalA map:^(NSNumber *num) {
return num * 10;
}];
10 30 20
Operators
1 3 2
[signalA filter:^(NSNumber *num) {
return num < 3;
}];
1 2
Operators
a b c
1 3 2
[signalA merge:signalB]
1 3 2a b c
Whats Next?
?
1. Reactive View Controller
2. Reactive Notifications
3. Functional Data
Processing
Something Simple
Authentication
/// Stores authentication credentials and tells us if we're authenticated.
@protocol AuthStore <NSObject>
@property (nonatomic, readonly) BOOL authenticated;
- (void)storeAccessCredentials:(AccessCredentials *)accessCredentials;
- (nullable AccessCredentials *)retrieveAccessCredentials;
- (void)removeAccessCredentials;
@end
Observe Authentication Changes
RACSignal *auth = RACObserve(authStore, authenticated);
YES NO YES
Select the Right View Controller
RACSignal *authSignal = RACObserve(authStore, authenticated)
// Pick a view controller
RACSignal *viewControllerSignal =
[authenticatedSignal map:^(NSNumber *isAuthenticated) {
if (isAuthenticated.boolValue)
return [DashboardViewController new];
} else {
return [LoginViewController new];
}
}];
Select the Right View Controller
YES NO YES
Dash
board
Login
Dash
board
map { // BOOL to ViewController }
Displaying it on Screen
How do we get the value out?
We have to subscribe, like a callback.
- (void)viewDidLoad {
[super viewDidLoad];
// Whenever we get a new view controller, PUSH IT
[[viewControllerSignal deliverOnMainThread]
subscribeNext:^(UIViewController *viewController) {
[self showViewController:viewController sender:self];
}];
}
Ok, maybe thats pushing it
Lets make a custom view controller
container!
Switching to the latest view controller
@interface SwitchingController : UIViewController
- (instancetype)initWithViewControllers:(RACSignal *)viewControllers;
@property (nonatomic, readonly) UIViewController *currentViewController;
@end
Manages a signal of view controllers, always
displaying the most recent one
- (void)viewDidLoad {
[super viewDidLoad];
[[self.viewControllers deliverOnMainThread]
subscribeNext:^(UIViewController *viewController) {
[self transitionFrom:self.currentViewController to:viewController];
}];
}
Setting Up
When the view loads, subscribe to our signal
- (void)transitionFrom:(UIViewController *)fromViewController
to:(UIViewController *)toViewController {
if (!fromViewController) {
[self addInitialViewController:toViewController];
return;
}
[self addChildViewController:nextViewController];
nextViewController.view.frame = self.view.bounds;
[self.view addSubview:nextViewController.view];
[previousViewController willMoveToParentViewController:nil];
[self transitionFromViewController:fromViewController
toViewController:toViewController
duration:0.5
options:UIViewAnimationOptionTransitionCrossDissolve
animations:nil
completion:^(BOOL finished) {
[toViewController didMoveToParentViewController:self];
[fromViewController removeFromParentViewController];
self.currentViewController = toViewController;
}];
}
Transitioning
YES NO YES
DashboardLogin
Finishing Up
What happens if the view controller
changes rapidly?
- (void)viewDidLoad {
[super viewDidLoad];
[[[self.viewControllers
throttle:0.5]
deliverOnMainThread]
subscribeNext:^(UIViewController *viewController) {
[self transitionFrom:self.currentViewController
to:viewController];
}];
}
Simple Throttling
Only take a new view controller if 0.5 seconds have
passed.
v2: Only throttle while animating
Keep track of when were animating
- (void)transitionFrom:(UIViewController *)fromViewController
to:(UIViewController *)toViewController {
// code
self.animating = YES;
[self transitionFromViewController:fromViewController
toViewController:toViewController
duration:0.5
options:UIViewAnimationOptionTransitionCrossDissolve
animations:nil
completion:^(BOOL finished) {
// more code
self.animating = NO;
}];
}
v2: Only throttle while animating
Throttle only if we are animating
[[[self.viewControllers
throttle:0.5 valuesPassingTest:^(id _) {
return self.isAnimating;
}]
deliverOnMainThread]
subscribeNext:^(UIViewController *viewController) {
[self transitionFrom:self.currentViewController to:viewController];
}];
What have we learned?
 Signals can represent real world events (Logging in/out)
 We can transform them using operators like map
 We can control timing through operators like throttle
1. Reactive View Controller
2. Reactive Notifications
3. Functional Data
Processing
Hot Signals
 Events happen regardless of any observers.
 Stream of *events* happening in the world.
 e.g. UI interaction, notifications
Cold Signals
 Subscribing starts the stream of events.
 Stream of results caused by some side effects.
 e.g. network calls, database transactions
Push Notifications
- (void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo {
// Do something horrible in here
}
A Better Option
typedef NS_ENUM(NSUInteger, NotificationType) {
NotificationTypeA,
NotificationTypeB,
NotificationTypeC,
};
@protocol NotificationProvider
- (RACSignal *)notificationSignalForNotificationType:
(NotificationType)type;
@end
We Want This:
@property id<NotificationProvider> notificationProvider;
- (void)viewDidLoad {
[[[self.notificationProvider
notificationSignalForNotificationType:NotificationTypeA]
deliverOnMainThread]
subscribeNext:^(Notification *notification) {
[self updateInterfaceWithNotification:notification];
}]
}
A New Friend!
Lift a selector into the reactive world
- (RACSignal *)rac_signalForSelector:(SEL)selector;
The returned signal will fire an event every time the
method is called.
Lets Do It!
- (RACSignal *)notificationSignalForNotificationType:
(NotificationType)type {
return [[[self
rac_signalForSelector:@selector(application:didReceiveRemoteNotification:)]
map:^(RACTuple *arguments) {
return arguments.second;
}]
map:^(NSDictionary *userInfo) {
// Parse our user info dictionary into a model object
return [self parseNotification:userInfo];
}]
filter:^(Notification *notification)
notification.type = type;
}];
}
NSDict NSDict NSDict
Notificati
on
A
Notificati
on
B
Notificati
on
A
map { [self parseNotification:userInfo]; }
filter { notification.type == typeA }
Notificati
on
A
Notificati
on
A
A Wild Local Notification Appears!
 We don't want to duplicate our current notification handling.
 Local and remote notifications should have the same effects.
RACSignal *remoteNotificationInfo = [[self
rac_signalForSelector:@selector(application:didReceiveRemoteNotification:)]
map:^(RACTuple *arguments) {
return arguments.second
}];
RACSignal *localNotificationInfo = [[self
rac_signalForSelector:@selector(application:didReceiveLocalNotification:)]
map:^(RACTuple *arguments) {
UILocalNotification *notification = arguments.second;
return notification.userInfo;
}];
return [[[remoteNotificationInfo merge:localNotificationInfo]
map:^(NSDictionary *userInfo) {
// Parse our user info dictionary into a model object
return [self parseNotification:userInfo];
}]
filter:^(Notification *notification) {
return notification.type == type;
}];
Problem
 We only get notifications sent *after* we subscribe.
 We can't easily update app state or UI that is created after
the notification is sent.
Solution?
[signal replayLast]
Whenever you subscribe to the signal, it will immediately
send you the most recent value from the stream.
Replay it
return [[[remoteNotificationInfo merge:localNotificationInfo]
map:^(NSDictionary *userInfo) {
// Parse our user info dictionary into a model object
return [self parseNotification:userInfo];
}]
filter:^(Notification *notification) {
return notification.type == type;
}]
replayLast];
return [[remoteNotificationInfo merge:localNotificationInfo]
map:^(NSDictionary *userInfo) {
// Parse our user info dictionary into a model object
return [self parseNotification:userInfo];
}]
filter:^(Notification *notification) {
return notification.type == type;
}];
What have we learned?
 Signals can model complex app behaviour like notifications
 We can combine signals in interesting ways
 Helpers like signalForSelector allow us to lift regular functions into signals
1. Reactive View Controller
2. Reactive Notifications
3. Functional Data
Processing
Describing an Algorithm with Functions
Example: Finding the best voucher to cover a
purchase
 If there are any vouchers with higher value than the purchase,
use the lowest valued of those
 Otherwise, use the highest valued voucher available
Imperative Approach
NSArray *sortedVouchers = [vouchers
sortedArrayUsingComparator:^NSComparisonResult(id<Voucher> voucher1,
id<Voucher> voucher2) {
return [voucher1 compare:voucher2];
}];
NSArray *vouchers = [[self voucherLibrary] vouchers];
id<Voucher> bestVoucher = nil;
for (id<Voucher> voucher in sortedVouchers) {
NSDecimalNumber *voucherAmount = voucher.amount;
if (!voucherAmount) continue;
if ([voucherAmount isLessThan:purchaseAmount]) {
bestVoucher = voucher;
} else if ([voucherAmount isGreaterThanOrEqualTo:purchaseAmount]) {
bestVoucher = voucher;
break;
}
}
return bestVoucher;
}
NSMutableArray *vouchersWithValue = [NSMutableArray array];
for (id<Voucher> voucher in sortedVouchers) {
if (voucher.amount) {
[vouchersWithValue addObject:voucher];
}
}
id<Voucher> bestVoucher = nil;
for (id<Voucher> voucher in vouchersWithValue) {
NSDecimalNumber *voucherAmount = voucher.amount;
if ([voucherAmount isLessThen:purchaseAmount]) {
bestVoucher = voucher;
} else if ([voucherAmount isGreaterThanOrEqualTo:purchaseAmount]) {
bestVoucher = voucher;
break;
}
}
Separate the Filter?
Functional Approach
- (id<Voucher>)voucherForPurchaseAmount:(NSDecimalNumber *)purchaseAmount {
[[[[[[[[self voucherLibrary]
vouchers]
sortedArrayUsingComparator:^NSComparisonResult(id<Voucher> voucher1,
id<Voucher> voucher2) {
return [voucher1 compare:voucher2];
}]
rac_sequence]
filter:^BOOL(id<Voucher> voucher) {
return voucher.amount != nil;
}]
yow_takeUptoBlock:^BOOL(id<Voucher> voucher) {
return [voucher.amount isGreaterThanOrEqualTo:purchaseAmount];
}]
array]
lastObject];
}
NSArray *sortedVouchers = [vouchers
sortedArrayUsingComparator:^NSComparisonResult(id<Voucher> voucher1,
id<Voucher> voucher2) {
return [voucher1 compare:voucher2];
}];
NSArray *vouchers = [[self voucherLibrary] vouchers];
id<Voucher> bestVoucher = nil;
for (id<Voucher> voucher in sortedVouchers) {
NSDecimalNumber *voucherAmount = voucher.amount;
if (!voucherAmount) continue;
if ([voucherAmount isLessThan:purchaseAmount]) {
bestVoucher = voucher;
} else if ([voucherAmount isGreaterThanOrEqualTo:purchaseAmount]) {
bestVoucher = voucher;
break;
}
}
return bestVoucher;
}
What have we learned?
 Functional code can be more readable
 It's easy to convert between imperative and functional
code using ReactiveCocoa
 Signals are lazy
1. Reactive View Controller
2. Reactive Notifications
3. Functional Data
Processing
Resources
 reactivecocoa.io
 ReactiveCocoa on GitHub
 ReactiveCocoa - The Definitive Introduction (Ray Wenderlich)
 A First Look at ReactiveCocoa 3.0 (Scott Logic)
Next Steps
 Try it out!
 Dont be scared to go all in
Thanks!
Questions?

More Related Content

ReactiveCocoa in Practice

  • 2. Jeames Bone Mark Corbyn @outware www.outware.com.au @jeamesbone @markcorbyn
  • 4. What is ReactiveCocoa? Functional Reactive Programming (FRP) framework for iOS and OSX applications.
  • 5. Why ReactiveCocoa? Instead of telling a computer how to do its job, why dont we just tell it what its job is, and let it figure the rest out.
  • 6. ReactiveCocoa in a Nutshell The main component in ReactiveCocoa is the Signal. Signals represent streams of values over time. 1 3 2 T
  • 7. Signals h he hel T [textField.rac_textSignal subscribeNext:^(NSString *text) { NSLog(@"text: %@", text); }]; > text: h > text: he > text: hel
  • 8. Operators 1 3 2 [signalA map:^(NSNumber *num) { return num * 10; }]; 10 30 20
  • 9. Operators 1 3 2 [signalA filter:^(NSNumber *num) { return num < 3; }]; 1 2
  • 10. Operators a b c 1 3 2 [signalA merge:signalB] 1 3 2a b c
  • 12. 1. Reactive View Controller 2. Reactive Notifications 3. Functional Data Processing
  • 13. Something Simple Authentication /// Stores authentication credentials and tells us if we're authenticated. @protocol AuthStore <NSObject> @property (nonatomic, readonly) BOOL authenticated; - (void)storeAccessCredentials:(AccessCredentials *)accessCredentials; - (nullable AccessCredentials *)retrieveAccessCredentials; - (void)removeAccessCredentials; @end
  • 14. Observe Authentication Changes RACSignal *auth = RACObserve(authStore, authenticated); YES NO YES
  • 15. Select the Right View Controller RACSignal *authSignal = RACObserve(authStore, authenticated) // Pick a view controller RACSignal *viewControllerSignal = [authenticatedSignal map:^(NSNumber *isAuthenticated) { if (isAuthenticated.boolValue) return [DashboardViewController new]; } else { return [LoginViewController new]; } }];
  • 16. Select the Right View Controller YES NO YES Dash board Login Dash board map { // BOOL to ViewController }
  • 17. Displaying it on Screen How do we get the value out? We have to subscribe, like a callback. - (void)viewDidLoad { [super viewDidLoad]; // Whenever we get a new view controller, PUSH IT [[viewControllerSignal deliverOnMainThread] subscribeNext:^(UIViewController *viewController) { [self showViewController:viewController sender:self]; }]; }
  • 18. Ok, maybe thats pushing it Lets make a custom view controller container!
  • 19. Switching to the latest view controller @interface SwitchingController : UIViewController - (instancetype)initWithViewControllers:(RACSignal *)viewControllers; @property (nonatomic, readonly) UIViewController *currentViewController; @end Manages a signal of view controllers, always displaying the most recent one
  • 20. - (void)viewDidLoad { [super viewDidLoad]; [[self.viewControllers deliverOnMainThread] subscribeNext:^(UIViewController *viewController) { [self transitionFrom:self.currentViewController to:viewController]; }]; } Setting Up When the view loads, subscribe to our signal
  • 21. - (void)transitionFrom:(UIViewController *)fromViewController to:(UIViewController *)toViewController { if (!fromViewController) { [self addInitialViewController:toViewController]; return; } [self addChildViewController:nextViewController]; nextViewController.view.frame = self.view.bounds; [self.view addSubview:nextViewController.view]; [previousViewController willMoveToParentViewController:nil]; [self transitionFromViewController:fromViewController toViewController:toViewController duration:0.5 options:UIViewAnimationOptionTransitionCrossDissolve animations:nil completion:^(BOOL finished) { [toViewController didMoveToParentViewController:self]; [fromViewController removeFromParentViewController]; self.currentViewController = toViewController; }]; } Transitioning
  • 23. Finishing Up What happens if the view controller changes rapidly?
  • 24. - (void)viewDidLoad { [super viewDidLoad]; [[[self.viewControllers throttle:0.5] deliverOnMainThread] subscribeNext:^(UIViewController *viewController) { [self transitionFrom:self.currentViewController to:viewController]; }]; } Simple Throttling Only take a new view controller if 0.5 seconds have passed.
  • 25. v2: Only throttle while animating Keep track of when were animating - (void)transitionFrom:(UIViewController *)fromViewController to:(UIViewController *)toViewController { // code self.animating = YES; [self transitionFromViewController:fromViewController toViewController:toViewController duration:0.5 options:UIViewAnimationOptionTransitionCrossDissolve animations:nil completion:^(BOOL finished) { // more code self.animating = NO; }]; }
  • 26. v2: Only throttle while animating Throttle only if we are animating [[[self.viewControllers throttle:0.5 valuesPassingTest:^(id _) { return self.isAnimating; }] deliverOnMainThread] subscribeNext:^(UIViewController *viewController) { [self transitionFrom:self.currentViewController to:viewController]; }];
  • 27. What have we learned? Signals can represent real world events (Logging in/out) We can transform them using operators like map We can control timing through operators like throttle
  • 28. 1. Reactive View Controller 2. Reactive Notifications 3. Functional Data Processing
  • 29. Hot Signals Events happen regardless of any observers. Stream of *events* happening in the world. e.g. UI interaction, notifications
  • 30. Cold Signals Subscribing starts the stream of events. Stream of results caused by some side effects. e.g. network calls, database transactions
  • 31. Push Notifications - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { // Do something horrible in here }
  • 32. A Better Option typedef NS_ENUM(NSUInteger, NotificationType) { NotificationTypeA, NotificationTypeB, NotificationTypeC, }; @protocol NotificationProvider - (RACSignal *)notificationSignalForNotificationType: (NotificationType)type; @end
  • 33. We Want This: @property id<NotificationProvider> notificationProvider; - (void)viewDidLoad { [[[self.notificationProvider notificationSignalForNotificationType:NotificationTypeA] deliverOnMainThread] subscribeNext:^(Notification *notification) { [self updateInterfaceWithNotification:notification]; }] }
  • 34. A New Friend! Lift a selector into the reactive world - (RACSignal *)rac_signalForSelector:(SEL)selector; The returned signal will fire an event every time the method is called.
  • 35. Lets Do It! - (RACSignal *)notificationSignalForNotificationType: (NotificationType)type { return [[[self rac_signalForSelector:@selector(application:didReceiveRemoteNotification:)] map:^(RACTuple *arguments) { return arguments.second; }] map:^(NSDictionary *userInfo) { // Parse our user info dictionary into a model object return [self parseNotification:userInfo]; }] filter:^(Notification *notification) notification.type = type; }]; }
  • 36. NSDict NSDict NSDict Notificati on A Notificati on B Notificati on A map { [self parseNotification:userInfo]; } filter { notification.type == typeA } Notificati on A Notificati on A
  • 37. A Wild Local Notification Appears! We don't want to duplicate our current notification handling. Local and remote notifications should have the same effects.
  • 38. RACSignal *remoteNotificationInfo = [[self rac_signalForSelector:@selector(application:didReceiveRemoteNotification:)] map:^(RACTuple *arguments) { return arguments.second }]; RACSignal *localNotificationInfo = [[self rac_signalForSelector:@selector(application:didReceiveLocalNotification:)] map:^(RACTuple *arguments) { UILocalNotification *notification = arguments.second; return notification.userInfo; }]; return [[[remoteNotificationInfo merge:localNotificationInfo] map:^(NSDictionary *userInfo) { // Parse our user info dictionary into a model object return [self parseNotification:userInfo]; }] filter:^(Notification *notification) { return notification.type == type; }];
  • 39. Problem We only get notifications sent *after* we subscribe. We can't easily update app state or UI that is created after the notification is sent.
  • 40. Solution? [signal replayLast] Whenever you subscribe to the signal, it will immediately send you the most recent value from the stream.
  • 41. Replay it return [[[remoteNotificationInfo merge:localNotificationInfo] map:^(NSDictionary *userInfo) { // Parse our user info dictionary into a model object return [self parseNotification:userInfo]; }] filter:^(Notification *notification) { return notification.type == type; }] replayLast]; return [[remoteNotificationInfo merge:localNotificationInfo] map:^(NSDictionary *userInfo) { // Parse our user info dictionary into a model object return [self parseNotification:userInfo]; }] filter:^(Notification *notification) { return notification.type == type; }];
  • 42. What have we learned? Signals can model complex app behaviour like notifications We can combine signals in interesting ways Helpers like signalForSelector allow us to lift regular functions into signals
  • 43. 1. Reactive View Controller 2. Reactive Notifications 3. Functional Data Processing
  • 44. Describing an Algorithm with Functions Example: Finding the best voucher to cover a purchase If there are any vouchers with higher value than the purchase, use the lowest valued of those Otherwise, use the highest valued voucher available
  • 46. NSArray *sortedVouchers = [vouchers sortedArrayUsingComparator:^NSComparisonResult(id<Voucher> voucher1, id<Voucher> voucher2) { return [voucher1 compare:voucher2]; }]; NSArray *vouchers = [[self voucherLibrary] vouchers]; id<Voucher> bestVoucher = nil; for (id<Voucher> voucher in sortedVouchers) { NSDecimalNumber *voucherAmount = voucher.amount; if (!voucherAmount) continue; if ([voucherAmount isLessThan:purchaseAmount]) { bestVoucher = voucher; } else if ([voucherAmount isGreaterThanOrEqualTo:purchaseAmount]) { bestVoucher = voucher; break; } } return bestVoucher; }
  • 47. NSMutableArray *vouchersWithValue = [NSMutableArray array]; for (id<Voucher> voucher in sortedVouchers) { if (voucher.amount) { [vouchersWithValue addObject:voucher]; } } id<Voucher> bestVoucher = nil; for (id<Voucher> voucher in vouchersWithValue) { NSDecimalNumber *voucherAmount = voucher.amount; if ([voucherAmount isLessThen:purchaseAmount]) { bestVoucher = voucher; } else if ([voucherAmount isGreaterThanOrEqualTo:purchaseAmount]) { bestVoucher = voucher; break; } } Separate the Filter?
  • 49. - (id<Voucher>)voucherForPurchaseAmount:(NSDecimalNumber *)purchaseAmount { [[[[[[[[self voucherLibrary] vouchers] sortedArrayUsingComparator:^NSComparisonResult(id<Voucher> voucher1, id<Voucher> voucher2) { return [voucher1 compare:voucher2]; }] rac_sequence] filter:^BOOL(id<Voucher> voucher) { return voucher.amount != nil; }] yow_takeUptoBlock:^BOOL(id<Voucher> voucher) { return [voucher.amount isGreaterThanOrEqualTo:purchaseAmount]; }] array] lastObject]; }
  • 50. NSArray *sortedVouchers = [vouchers sortedArrayUsingComparator:^NSComparisonResult(id<Voucher> voucher1, id<Voucher> voucher2) { return [voucher1 compare:voucher2]; }]; NSArray *vouchers = [[self voucherLibrary] vouchers]; id<Voucher> bestVoucher = nil; for (id<Voucher> voucher in sortedVouchers) { NSDecimalNumber *voucherAmount = voucher.amount; if (!voucherAmount) continue; if ([voucherAmount isLessThan:purchaseAmount]) { bestVoucher = voucher; } else if ([voucherAmount isGreaterThanOrEqualTo:purchaseAmount]) { bestVoucher = voucher; break; } } return bestVoucher; }
  • 51. What have we learned? Functional code can be more readable It's easy to convert between imperative and functional code using ReactiveCocoa Signals are lazy
  • 52. 1. Reactive View Controller 2. Reactive Notifications 3. Functional Data Processing
  • 53. Resources reactivecocoa.io ReactiveCocoa on GitHub ReactiveCocoa - The Definitive Introduction (Ray Wenderlich) A First Look at ReactiveCocoa 3.0 (Scott Logic)
  • 54. Next Steps Try it out! Dont be scared to go all in