Mobile Development 11 min read

Understanding Cold and Hot Signals in ReactiveCocoa (RAC)

Understanding ReactiveCocoa’s cold and hot signals reveals that each subscription to a cold fetchData signal triggers a separate network request, causing duplicated calls, while converting the signal to hot with shareReplay or publish ensures a single request is shared across UI bindings and error handling.

Meituan Technology Team
Meituan Technology Team
Meituan Technology Team
Understanding Cold and Hot Signals in ReactiveCocoa (RAC)

ReactiveCocoa (RAC) is a Functional Reactive Programming (FRP) framework for Cocoa originally created by the GitHub team. The article introduces the distinction between cold and hot signals and explains why this distinction matters in iOS development.

It first reviews the notion of a pure function— a function whose return value depends only on its inputs and has no observable side effects— and lists typical side‑effects such as modifying global variables, sending notifications, performing I/O, or being affected by thread locks.

The article then presents a realistic Objective‑C example that uses RAC to fetch data, extract title and desc fields, render the description, and bind the results to UI elements. The full code is shown below:

self.sessionManager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"http://api.xxxx.com"]];
self.sessionManager.requestSerializer = [AFJSONRequestSerializer serializer];
self.sessionManager.responseSerializer = [AFJSONResponseSerializer serializer];
@weakify(self)
RACSignal *fetchData = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    @strongify(self)
    NSURLSessionDataTask *task = [self.sessionManager GET:@"fetchData" parameters:@{@"someParameter": @"someValue"} success:^(NSURLSessionDataTask *task, id responseObject) {
        [subscriber sendNext:responseObject];
        [subscriber sendCompleted];
    } failure:^(NSURLSessionDataTask *task, NSError *error) {
        [subscriber sendError:error];
    }];
    return [RACDisposable disposableWithBlock:^{
        if (task.state != NSURLSessionTaskStateCompleted) {
            [task cancel];
        }
    }];
}];

RACSignal *title = [fetchData flattenMap:^RACSignal *(NSDictionary *value) {
    if ([value[@"title"] isKindOfClass:[NSString class]]) {
        return [RACSignal return:value[@"title"]];
    } else {
        return [RACSignal error:[NSError errorWithDomain:@"some error" code:400 userInfo:@{ @"originData": value }]];
    }
}];

RACSignal *desc = [fetchData flattenMap:^RACSignal *(NSDictionary *value) {
    if ([value[@"desc"] isKindOfClass:[NSString class]]) {
        return [RACSignal return:value[@"desc"]];
    } else {
        return [RACSignal error:[NSError errorWithDomain:@"some error" code:400 userInfo:@{ @"originData": value }]];
    }
}];

RACSignal *renderedDesc = [desc flattenMap:^RACStream *(NSString *value) {
    NSError *error = nil;
    RenderManager *renderManager = [[RenderManager alloc] init];
    NSAttributedString *rendered = [renderManager renderText:value error:&error];
    if (error) {
        return [RACSignal error:error];
    } else {
        return [RACSignal return:rendered];
    }
}];

RAC(self.someLablel, text) = [[title catchTo:[RACSignal return:@"Error"]] startWith:@"Loading..."];
RAC(self.originTextView, text) = [[desc catchTo:[RACSignal return:@"Error"]] startWith:@"Loading..."];
RAC(self.renderedTextView, attributedText) = [[renderedDesc catchTo:[RACSignal return:[[NSAttributedString alloc] initWithString:@"Error"]]] startWith:[[NSAttributedString alloc] initWithString:@"Loading..."]];

[[RACSignal merge:@[title, desc, renderedDesc]] subscribeError:^(NSError *error) {
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error" message:error.domain delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
    [alertView show];
}];

The code demonstrates how RAC can chain network requests, error handling, and UI binding, but it also unintentionally triggers six network requests due to the cold nature of the fetchData signal.

Key observations:

The fetchData signal is cold; each subscription executes its side‑effect (the HTTP request).

Calling flattenMap on fetchData creates new signals ( title, desc) that subscribe to the original signal when they themselves are subscribed.

The UI bindings ( RAC(...)) and the final merge cause three separate subscriptions to fetchData, resulting in three requests.

The merge call creates another signal that, when subscribed, subscribes to its constituent signals, adding three more requests.

A simplified pseudo‑implementation of flattenMap is provided to illustrate how each transformation implicitly subscribes to the source signal:

- (instancetype)flattenMap_:(RACStream * (^)(id value))block {
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        return [self subscribeNext:^(id x) {
            RACSignal *signal = (RACSignal *)block(x);
            [signal subscribeNext:^(id x) {
                [subscriber sendNext:x];
            } error:^(NSError *error) {
                [subscriber sendError:error];
            } completed:^{
                [subscriber sendCompleted];
            }];
        } error:^(NSError *error) {
            [subscriber sendError:error];
        } completed:^{
            [subscriber sendCompleted];
        }];
    }];
}

Because the cold fetchData signal is subscribed multiple times, the same network request is executed repeatedly, which can lead to duplicated user actions (e.g., multiple likes) and unnecessary server load.

The article suggests converting the cold signal to a hot signal (e.g., using shareReplay or publish) to ensure the request is performed only once and the result is shared among all downstream subscribers.

Finally, the discussion touches on the concept of referential transparency in pure functional languages (e.g., Haskell) where identical inputs yield cached results, a property not available in Objective‑C, making cold signals a performance concern in iOS development.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

iOSObjective‑CfrpColdSignalHotSignalReactiveCocoasideEffects
Meituan Technology Team
Written by

Meituan Technology Team

Over 10,000 engineers powering China’s leading lifestyle services e‑commerce platform. Supporting hundreds of millions of consumers, millions of merchants across 2,000+ industries. This is the public channel for the tech teams behind Meituan, Dianping, Meituan Waimai, Meituan Select, and related services.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.