ReactiveCocoa自述:工作原理和應用

jopen 9年前發布 | 24K 次閱讀 iOS開發 移動開發 ReactiveCocoa

本文翻譯自GitHub上的開源框架ReactiveCocoa的readme,

英文原文鏈接https://github.com/ReactiveCocoa/ReactiveCocoa.

ReactiveCocoa (RAC)是一個Objective-C的框架,它的靈感來自函數式響應式編程.

如果你已經很熟悉函數式響應式編程編程或者了解ReactiveCocoa的一些基本前提,check outDocumentation文件夾作為框架的概述,這里面有一些關于它怎么工作的深層次的信息.

感謝Rheinfabrik對ReactiveCocoa 3!_開發慷慨地贊助.

什么是ReactiveCocoa?

ReactiveCocoa文檔寫得很厲害,并且詳細地介紹了RAC是什么以及它是怎么工作的?

如果你多學一點,我們推薦下面這些資源:

  1. Introduction

    </li>

  2. When to use ReactiveCocoa

    </li>

  3. Framework Overview

    </li>

  4. Basic Operators

    </li>

  5. Header documentation

    </li>

  6. Previously answered Stack Overflow questions and GitHub issues

    </li>

  7. The rest of the Documentation folder

    </li>

  8. Functional Reactive Programming on iOS(eBook)

    </li> </ol>

    如果你有任何其他的問題,請隨意提交issue,

    file an issue.

    介紹

    ReactiveCocoa的靈感來自函數式響應式編程.Rather than using mutable variables which are replaced and modified in-place,RAC提供signals(表現為RACSignal)來捕捉當前以及將來的值.

    通過對signals進行連接,綁定和響應,不需要連續地觀察和更新值,軟件就能寫了.

    舉個例子,一個text field能夠綁定到最新狀態,即使它在變,而不需要用額外的代碼去更新text field每一秒的狀態.它有點像KVO,但它用blocks代替了重寫-observeValueForKeyPath:ofObject:change:context:.

    Signals也能夠呈現異步的操作,有點像futures and promises.這極大地簡化了異步軟件,包括了網絡處理的代碼.

    RAC有一個主要的優點,就是提供了一個單一的,統一的方法去處理異步的行為,包括delegate方法,blocks回調,target-action機制,notifications和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 notifications,signals能夠連接在一起并且能夠同時進行操作:

    // 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);
        }];

    Signals也能夠用來導出狀態.而不是observing properties或者設置其他的 properties去反應新的值,RAC通過signals and operations讓表示屬性變得有可能:

    // 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]);
        }];

    Signals不僅僅能夠用在KVO,還可以用在很多的地方.比如說,它們也能夠展示button presses:

    // 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;

    Signals能夠展示timers,其他的UI事件,或者其他跟時間改變有關的東西.

    對于用signals來進行異步操作,通過連接和改變這些signals能夠進行更加復雜的行為.在一組操作完成時,工作能夠很簡單觸發:

    // 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!");
        }];

    Signals能夠順序地執行異步操作,而不是嵌套block回調.這個和futures and promises很相似:

    // 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,但我盡可能用更少的代碼,更少的模版,把更好的代碼去表達清楚.

    如果想要更多的示例代碼,可以check outC-41 或者 GroceryList,這些都是真正用ReactiveCocoa寫的iOS apps.更多的RAC信息可以看一下Documentation文件夾.

    什么時候用ReactiveCocoa

    乍看上去,ReactiveCocoa是很抽象的,它可能很難理解如何將它應用到具體的問題.

    這里有一些RAC常用的地方.

    處理異步或者事件驅動數據源

    很多Cocoa編程集中在響應user events或者改變application state.這樣寫代碼很快地會變得很復雜,就像一個意大利面,需要處理大量的回調和狀態變量的問題.

    這個模式表面上看起來不同,像UI回調,網絡響應,和KVO notifications,實際上有很多的共同之處。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];
    }];

    ReactiveCocoa 則讓這種模式特別簡單:

    [[[[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];

    上面的代碼能夠簡單地用合成signals來清理和優化:

    RACSignal *databaseSignal = [[databaseClient
        fetchObjectsMatchingPredicate:predicate]
        subscribeOn:[RACScheduler scheduler]];
    RACSignal *fileSignal = [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id 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");
        }];

    簡化集合轉換

    像map, filter, fold/reduce 這些高級功能在Foundation中是極度缺少的m導致了一些像下面這樣循環集中的代碼:

    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"];
        }];

    系統要求

    ReactiveCocoa 要求 OS X 10.8+ 以及 iOS 8.0+.

    引入 ReactiveCocoa

    增加 RAC 到你的應用中:

    1. 增加 ReactiveCocoa 倉庫 作為你應用倉庫的一個子模塊.

    2. 從ReactiveCocoa文件夾中運行 script/bootstrap .

    3. 拖拽 ReactiveCocoa.xcodeproj 到你應用的 Xcode project 或者 workspace中.

    4. 在你應用target的"Build Phases"的選項卡,增加 RAC到 "Link Binary With Libraries"

    On iOS, 增加 libReactiveCocoa-iOS.a.

    On OS X, 增加 ReactiveCocoa.framework.

    RAC 必須選擇"Copy Frameworks" . 假如你沒有的話, 需要選擇"Copy Files"和"Frameworks" .

    5. 增加 "$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/include"

    $(inherited)到 "Header Search Paths" (這需要archive builds, 但也沒什么影響).

    6. For iOS targets, 增加 -ObjC 到 "Other Linker Flags" .

    7. 假如你增加 RAC到一個project (不是一個workspace), 你需要適當的添加RAC target到你應用的"Target Dependencies".

    假如你喜歡用CocoaPods,這里有一些慷慨地第三方貢獻ReactiveCocoa podspecs .

    想看一個用了RAC的工程,check outC-41 或者 GroceryList,這些是真實的用ReactiveCocoa寫的iOS apps.

    獨立開發

    假如你的工作用RAC是隔離的而不是將其集成到另一個項目,你會想打開ReactiveCocoa.xcworkspace 而不是.xcodeproj.

    更多信息

    ReactiveCocoa靈感來自.NET的ReactiveExtensions (Rx).Rx的一些原則也能夠很好的用在RAC.這里有些好的Rx資源:

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!