ReactiveObjC Documentation

repository·master·Indexed 25 days ago

https://github.com/reactivecocoa/reactiveobjc

An Objective-C framework for Functional Reactive Programming (FRP) that provides tools to compose and transform streams of values using RACSignal and RACSequence. It unifies asynchronous and event-driven data sources—such as KVO, NSNotificationCenter, and UI callbacks—into a single declarative approach. The framework includes operators for mapping, filtering, merging, and flattening streams, as well as guidelines for scheduler-based subscription and resource management.

Tokens
7.3K
Snippets
13
Records
45
Agent score
83%

What's inside ReactiveObjC

  1. Introduction to ReactiveObjC

    master
    ReactiveObjC (formerly ReactiveCocoa) is an Objective-C framework inspired by Functional Reactive Programming. It provides APIs for composing and transforming streams of values using RACSignal objects. Instead of using mutable variables that are modified in-place, you use signals that capture present and future values. This allows for declarative programming, simplifying asynchronous behaviors like delegate methods, callback blocks, target-action mechanisms, notifications, and KVO into a single, unified approach.
  2. Understand Signals and Event Types

    master

    A signal (RACSignal) is a push-driven stream of data delivered in the future. Users must subscribe to a signal to receive its values. A signal's lifetime consists of zero or more next events, followed by exactly one error or completed event.

    Signals emit three types of events:

    • next: Provides a new value. Unlike Cocoa collections, signals can include nil.
    • error: Indicates an error occurred (includes an NSError). This terminates the signal.
    • completed: Indicates the signal finished successfully. This terminates the signal.
  3. Unify asynchronous and event-driven data sources

    master

    ReactiveObjC unifies disparate Cocoa APIs like UI callbacks, network responses, and KVO notifications into RACSignal objects. This allows you to compose them using operators like combineLatest:, RACObserve, and rac_signalForControlEvents:.

    Key patterns include:

    • Using RACObserve(object, keyPath) for KVO.
    • Using rac_addObserverForName:object: for NSNotificationCenter.
    • Using rac_textSignal for text field changes.
    • Using rac_signalForControlEvents: for UI events.
    // Example: Combining multiple signals to drive UI state
    @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);
        }];
  4. Implement new ReactiveObjC operators

    master

    When implementing custom operators for RACStream, RACSequence, or RACSignal, follow these best practices to ensure stability and compatibility:

    • Prefer RACStream methods: Implement new operators using RACStream methods whenever possible. RACStream provides a simpler interface, and its operators are automatically applicable to both sequences and signals. Key methods like -bind:, -zipWith:, and -concat: are powerful enough for most tasks.
    • Use -materialize for error handling: If a RACSignal operator needs to handle error or completed events, use the -materialize method to bring these events into the stream. This allows you to use standard stream operators to manipulate them.
    • Compose existing operators: Avoid writing logic from scratch. Use the built-in, tested RAC operators to minimize duplication and bugs.
    • Avoid introducing concurrency: Operators should not perform work concurrently. Callers can manage concurrency by subscribing or delivering events on a specific RACScheduler.
  5. Deliver signal events onto a specific RACScheduler

    master
    Use the -deliverOn: operator to ensure signal events arrive on a specific RACScheduler (e.g., the main thread for UI updates). To minimize performance overhead and delays, restrict the use of -deliverOn: to the end of a signal chain, such as right before subscription or binding to a property.
  6. Prevent stack overflow in recursive operators

    master

    If an operator requires recursion (e.g., a repeat operator), do not use standard recursive function calls, as this can lead to stack overflow and crashes. Instead, use the -scheduleRecursiveBlock: method of RACScheduler. This transforms recursion into iteration, protecting the call stack.

    // Correct implementation of repeat using scheduleRecursiveBlock to avoid stack overflow
    - (RACSignal *)repeat {
        return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
            RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable];
    
            RACScheduler *scheduler = RACScheduler.currentScheduler ?: [RACScheduler scheduler];
            RACDisposable *disposable = [scheduler scheduleRecursiveBlock:^(void (^reschedule)(void)) {
                RACDisposable *subDisposable = [self subscribeNext:^(id x) {
                    [subscriber sendNext:x];
                } error:^(NSError *error) {
                    [subscriber sendError:error];
                } completed:^{
                    reschedule();
                }];
    
                [compoundDisposable addDisposable:subDisposable];
            }];
    
            [compoundDisposable addDisposable:disposable];
            return compoundDisposable;
        }];
    }
  7. Maintain type homogeneity in streams

    master
    While RACStream, RACSignal, and RACSequence allow heterogeneous objects, using different types in a single stream complicates operator usage and forces consumers to perform manual type checking. Whenever possible, ensure streams contain only objects of the same type.
  8. Avoid blocking in stream operators

    master

    Stream operators should return a new stream almost immediately. Any heavy lifting or work required by the operator should be part of the evaluation of the new stream, not part of the operator invocation itself.

    Incorrect approach: Performing work (like a loop) inside the operator method and returning a constructed result. Correct approach: Using existing operators (like flattenMap:) to return a new stream that performs the work when evaluated.

    // WRONG!
    - (RACSequence *)map:(id (^)(id))block {
        RACSequence *result = [RACSequence empty];
        for (id obj in self) {
            id mappedObj = block(obj);
            result = [result concat:[RACSequence return:mappedObj]];
        }
    
        return result;
    }
    
    // Right!
    - (RACSequence *)map:(id (^)(id))block {
        return [self flattenMap:^(id obj) {
            id mappedObj = block(obj);
            return [RACSequence return:mappedObj];
        }];
    }
  9. Understand the RACSequence contract

    master

    A RACSequence is a pull-driven stream that behaves similarly to collections. Developers should be aware of three core behaviors:

    1. Lazy Evaluation: Sequences are evaluated lazily by default. Work (like mapping or transformations) is only performed when a value is actually requested (e.g., accessing .head). Once a value is evaluated, it is memoized and won't be recalculated.
    2. Blocking Evaluation: Evaluating any part of a sequence will block the calling thread until the value is synchronously retrieved. For expensive operations, use [-signalWithScheduler:][RACSequence] to convert it to a signal instead.
    3. Single Side Effects: Side effects within a sequence operator occur only once per value—specifically when that value is first evaluated. Subsequent accesses to the same value or derived sequences will not re-trigger the side effect.
    NSArray *strings = @[ @"A", @"B", @"C" ];
    RACSequence *sequence = [strings.rac_sequence map:^(NSString *str) {
        return [str stringByAppendingString:@"_"];
    }];
    // No concatenation has happened yet. Accessing sequence.head triggers it.
  10. Format stream operations with consistent indentation

    master

    To improve readability in stream-heavy code, align transformation steps. When transforming a single stream multiple times, ensure all operators are aligned. For complex operators like +zip:reduce: or +combineLatest:reduce:, split them over multiple lines.

    RACStream *result = [[[RACStream
        zip:@[ firstStream, secondStream ]
        reduce:^(NSNumber *first, NSNumber *second) {
            return @(first.integerValue + second.integerValue);
        }]
        filter:^ BOOL (NSNumber *value) {
            return value.integerValue >= 0;
        }]
        map:^(NSNumber *value) {
            return @(value.integerValue + 1);
        }];
  11. Manage stream retention and memory

    master

    Avoid retaining RACStream or RACSignal longer than necessary to prevent retaining dependencies and increasing memory usage.

    For RACSequence, retain only as long as the head is needed. If the head is no longer required, retain the tail of the node instead of the node itself.

  12. Use descriptive declarations for signal-returning methods and properties

    master

    To make signal semantics clear, follow these naming conventions based on the signal's nature:

    1. Hot signals without side effects: Use properties named after events (e.g., textChanged). This indicates no initialization is needed and additional subscribers won't change semantics.
    2. Cold signals without side effects: Use methods with noun-like names (e.g., -currentText). This hints that work is performed at the time of subscription. Use plural nouns if the signal sends multiple values (e.g., -currentModels).
    3. Signals with side effects: Use methods with verb-like names (e.g., -logIn). This indicates the method is not idempotent and callers should be careful. If it sends values, include a noun describing them (e.g., -loadConfiguration).