RACifying Non-Reactive Code – Dave Lee’s Presentation Code

Dave Lee gave a talk titled RACifying Non-Reactive Code for GitHub Reactive Cocoa Developer Conference in June 2014 (about two weeks after Apple’s Swift WWDC announcement). His presentation is a no-nonsense, fast paced ~25 minutes of walking thru the multitude of ways in which objective-c, cocoa, and the Apple frameworks present events and how to translate those events into RAC (e.g., RACSignal).

One of the first things every newcomer to RAC learns – perhaps the first “a-ha!” moment – is that cocoa presents a multitude of ways in which events are passed and received. It’s eye opening to realize how much these seemingly different event mechanisms have in common. When learning about KVO for example, you don’t consider the commonality KVO has with another eventing mechanism such as delegation or target/action. But stepping back to look at these event paradigms together, to understand what they are ultimately trying to solve, makes you realize they indeed are all doing fundamentally the same thing – an event source is providing events to another object observing those events.

Now, this is one of the fundamental goals the RAC authors are trying to achieve : abstracting the 20+ years of cruft and ugliness built up in the various Objective-C eventing patterns into common, modern, and functional abstractions (i.e., RACSignal, RACSubscriber, etc) generic benicar. While their implementation is not perfect (“hot” vs. “cold” signals, a non-generic type system preventing you from clearly seeing what type will be delivered with an event), it goes a long way towards writing clear, concise code with less state (win, win, win!).

Dave’s talk was interesting since it showed concrete examples of the various objective-c patterns and how they translate into RAC. As I was watching the video, I was frequently pausing it, thinking to myself “oh, I can use that in this project – I need to copy this code down”. After stopping a few times, I realized I should just start over, copy all the examples down, and keep them for reference. The objective-c patterns he converts to RAC are:

  • Blocks
  • Delegates
  • Notifications
  • Errors
  • Target-Action
  • KVO
  • Method Overriding

Dave’s talk is highly recommended for anyone new to RAC. Without further ado, here’s the code:

 

 
// Blocks -> Signal
 
// Groups code that happens on subscriptions, returns a single value when block
// executes. defer is a general pattern to turn a function into a signal.
//
// This can be used to enclose a long-running function into an
// async signal.
 
[RACSignal defer:^ {
  // Test for sending a value and completing
  return [RACSignal return:@(arc4random())];
}];
 
// The same thing as above can be done (more explicitly)
// with createSignal
 
[RACSignal createSignal:^(id(<RACSubscriber> subscriber) {
  // Perform per-subscription side effects.
  [subscription sendNext:@(arc4random())];
  [subscription sendCompleted];
  return nil;
}];
 
// Blocks -> Signal
 
// AFNetworking Example:
 
[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
  [manager GET:URLString parameters:params
    success:^(AFHTTPRequestOperation *op, id response) {
      [subscriber sendNext:response];
      [subscriber sendCompleted];
    }
    failure:^(AFHTTPRequestOperation *op, NSError *e) {
      [subscriber:sendError:e];
    }];
}];
 
// Using disposables w/ operations to cancel
 
[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
  NSOperation *operation = [manager GET:URLString parameters:params
    success:^(AFHTTPRequestOperation *op, id response) {
      [subscriber sendNext:response];
      [subscriber sendCompleted];
    } failure:^(AFHTTPRequestOperation *op, NSError *error) {
      [subscriber sendError:e];
    }];
  return [RACDisposable disposableWithBlock:^ {
    [operation cancel];
  }];
}];
 
 
// Core Data : switchToLatest on a search request : automatically cancel old requests.
 
[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
  RACDisposable *disposable = [RACDisposable new];
 
  [managedObjectContext performBlock:^ {
    if (disposable.disposed) return;
 
    NSError *error;
    NSArray *results = [moc performFetch:fetchRequest error:&error];
 
    if (results != nil) {
      [subscriber sendNext:results];
      [subscriber sendCompleted];
    } else {
      [subscriber sendError:error];
    }
  }];
  return disposable;
}];
 
 
// Delegates : creating a signal version of a delegate.
 
// Shows the general pattern of wrapping delegate callbacks into
// signals. Also how to perform side-effect actions based on the
// subscriber count. In this example, CL is turned on when the
// first subscriber subscribes and turned off after all subscribers
// have unsubscribed.
//
// Notice we are setting `self` as the CL delegate but the
// delegate methods are not implemented - rather subscribed
// to via rac_signalForSelector. This translates
// delegate callbacks into signal values.
 
// reduceEach is like map. In this case, we use it to return only
// the values in the tuple that matter (the locations / error object).
 
// flattenMap takes a value and creates a signal from it.
 
CLLocationManager *locationManager = ...
locationManager.delegate = self; //
static volatile int32_t subscriberCount = 0;
 
[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
  RACSignal *locations = [[self rac_signalForSeletor:(@selector(...didUpdateLocations:)
    fromProtocol:@protocol(CLLocationManagerDelegate)]
    reduceEach^(id _, NSArray *locations) {
      return locations;
    }];
 
  RACSignal *error = [[self rac_signalForSeletor:(@selector(...didFailWithError:)
    fromProtocol:@protocol(CLLocationManagerDelegate)]
    reduceEach^(id _, NSError *error) {
      return error;
    }]
    filter:^BOOL (NSError *error) {
      // Documentation says CL will keep trying after kCLErrorLocationUnknown
      return error.code != kCLErrorLocationUnknown;
    }]
    flattenMap:^(NSError *error){
      return [RACSignal error:error]; // create a new signal that will send error.
    }];
 
    RACDisposable *disposable = [[RACSignal
      merge:@[ locations, error ]]
      subscribe:subscriber];
 
    // manage side effects if you have multiple subscribers
    if (OSAtomicIncrement32(&subscriberCount) == 1) {
      [locationManager startUpdatingLocation];
    } else {
      [subscriber sendNext:locationManager.location];
    }
 
    return [RACDisposable disposableWithBlock:^{
      [disposable dispose];
      if (OSAtomicDecrement32(&subscriberCount) == 0) {
        [locationManager stopUpdateLocation];
      }
    }];
}];
 
 
 
// KVO
 
RACSignal *isReachable = [RACObserve(reachabilityManager, networkReachabilityStatus)
  map:^(NSNumber *networkReachabilityStatus) {
    switch (networkReachabilityStatus.intValue) {
      case AFNetworkReachabilityStatusReachableViaWWAN:
      case AFNetworkReachabilityStatusReachableViaWiFi:
        return @YES;
    }
    return @NO;
  }];
 
// Notifications
 
RACSignal *isForeground = [RACSignal merge:@[
  [[defaultCenter rac_addObserverForName:WillEnterForeground ...]
    mapReplace:@YES]
  [[defaultCenter rac_addObserverForName:DidEnterBackground ...]
    mapReplace:@NO]
]];
 
 
// Listens to the foreground. When isForeground == @YES, sends values from
// the "didBecomeActive" signal. You can use this from a VM
// to *not* bind to UIApplicationDelegate
RACSignal *hasLaunchedActive = [RACSignal
  if:isForeground
  then:[defaultCenter rac_addObserverForName:DidBecomeActive]
  else:[RACSignal empty]];

Comments are closed, but trackbacks and pingbacks are open.