ReactiveCocoa 和 MVVM 入門

ReactiveCocoa 簡單介紹

  • 觀察值

你別動,你一動我就知道。

@weakify(self);
[RACObserve(self, value) subscribeNext:^(NSString* x) {
    @strongify(self);
    NSLog(@"你動了");
}];
  • 單邊

你唱歌,我就跳舞

RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"唱歌"];
        [subscriber sendCompleted];
        return nil;
    }];
    RAC(self, value) = [signalA map:^id(NSString* value) {
        if ([value isEqualToString:@"唱歌"]) {
            return @"跳舞";
        }
        return @"";
    }];
  • 雙邊

你向西,他就向東,他向左,你就向右。

RACChannelTerminal *channelA = RACChannelTo(self, valueA);
    RACChannelTerminal *channelB = RACChannelTo(self, valueB);
    [[channelA map:^id(NSString *value) {
        if ([value isEqualToString:@"西"]) {
            return @"東";
        }
        return value;
    }] subscribe:channelB];
    [[channelB map:^id(NSString *value) {
        if ([value isEqualToString:@"左"]) {
            return @"右";
        }
        return value;
    }] subscribe:channelA];
    [[RACObserve(self, valueA) filter:^BOOL(id value) {
        return value ? YES : NO;
    }] subscribeNext:^(NSString* x) {
        NSLog(@"你向%@", x);
    }];
    [[RACObserve(self, valueB) filter:^BOOL(id value) {
        return value ? YES : NO;
    }] subscribeNext:^(NSString* x) {
        NSLog(@"他向%@", x);
    }];
    self.valueA = @"西";
    self.valueB = @"左";
  • 代理

你是程序員,你幫我寫個app吧

@protocol Programmer <NSObject>
- (void)makeAnApp;
@end
RACSignal *ProgrammerSignal =  
[self rac_signalForSelector:@selector(makeAnApp)
               fromProtocol:@protocol(Programmer)];
[ProgrammerSignal subscribeNext:^(RACTuple* x) {
    NSLog(@"花了一個月,app寫好了");
}];
[self makeAnApp];

RACSignal *ProgrammerSignal =  
[self rac_signalForSelector:@selector(makeAnApp)
               fromProtocol:@protocol(Programmer)];
[ProgrammerSignal subscribeNext:^(RACTuple* x) {
    NSLog(@"花了一個月,app寫好了");
}];
[self makeAnApp];
  • 廣播

知道你的頻道,我就能聽到你了。

[[[NSNotificationCenter defaultCenter] rac_addObserverForName:@"代碼之道頻道" object:nil] subscribeNext:^(NSNotification* x) {
        NSLog(@"技巧:%@", x.userInfo[@"技巧"]);
    }];
    [[NSNotificationCenter defaultCenter] postNotificationName:@"代碼之道頻道" object:nil userInfo:@{@"技巧":@"用心寫"}];
  • 節(jié)流

不好意思,這里一秒鐘只能通過一個人

[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"旅客A"];
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [subscriber sendNext:@"旅客B"];
        });
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [subscriber sendNext:@"旅客C"];
            [subscriber sendNext:@"旅客D"];
            [subscriber sendNext:@"旅客E"];
        });
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [subscriber sendNext:@"旅客F"];
        });
        return nil;
    }] throttle:1] subscribeNext:^(id x) {
        NSLog(@"%@通過了",x);
    }];

// Test[2618:83764] 旅客A  
//Test[2618:83764] 旅客B  
// Test[2618:83764] 旅客E  
// Test[2618:83764] 旅客F
  • 連接

生活是一個故事接一個故事

RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"我戀愛啦"];
        [subscriber sendCompleted];
        return nil;
    }];
    RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"我結婚啦"];
        [subscriber sendCompleted];
        return nil;
    }];
    [[signalA concat:signalB] subscribeNext:^(id x) {
        NSLog(@"%@",x);
    }];
  • 合并

污水都應該流入污水處理廠被處理

RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"紙廠污水"];
        return nil;
    }];
    RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"電鍍廠污水"];
        return nil;
    }];
    [[RACSignal merge:@[signalA, signalB]] subscribeNext:^(id x) {
        NSLog(@"處理%@",x);
    }];
  • 映射

我可以點石成金。

RACSignal *signal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"石"];
        return nil;
    }] map:^id(NSString* value) {
        if ([value isEqualToString:@"石"]) {
            return @"金";
        }
        return value;
    }];
    [signal subscribeNext:^(id x) {
        NSLog(@"%@", x);
    }];
  • 過濾

未滿十八歲,禁止進入。

[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@(15)];
        [subscriber sendNext:@(17)];
        [subscriber sendNext:@(21)];
        [subscriber sendNext:@(14)];
        [subscriber sendNext:@(30)];
        return nil;
    }] filter:^BOOL(NSNumber* value) {
        return value.integerValue >= 18;
    }] subscribeNext:^(id x) {
        NSLog(@"%@", x);
    }];
    }];
  • 扁平

打蛋液,煎雞蛋,上盤。

[[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        NSLog(@"打蛋液");
        [subscriber sendNext:@"蛋液"];
        [subscriber sendCompleted];
        return nil;
    }] flattenMap:^RACStream *(NSString* value) {
        return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            NSLog(@"把%@倒進鍋里面煎",value);
            [subscriber sendNext:@"煎蛋"];
            [subscriber sendCompleted];
            return nil;
        }];
    }] flattenMap:^RACStream *(NSString* value) {
        return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            NSLog(@"把%@裝到盤里", value);
            [subscriber sendNext:@"上菜"];
            [subscriber sendCompleted];
            return nil;
        }];
    }] subscribeNext:^(id x) {
        NSLog(@"%@", x);
    }];
  • 重放

一次制作,多次觀看。

RACSignal *replaySignal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        NSLog(@"大導演拍了一部電影《我的男票是程序員》");
        [subscriber sendNext:@"《我的男票是程序員》"];
        return nil;
    }] replay];
    [replaySignal subscribeNext:^(id x) {
        NSLog(@"小明看了%@", x);
    }];
    [replaySignal subscribeNext:^(id x) {
        NSLog(@"小紅也看了%@", x);
    }];


//Test[1854:54712] 大導演拍了一部電影《我的男票是程序員》  
//Test[1854:54712] 小明看了《我的男票是程序員》  
//Test[1854:54712] 小紅也看了《我的男票是程序員》
  • 重試

成功之前可能需要數(shù)百次失敗

__block int failedCount = 0;
    [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        if (failedCount < 100) {
            failedCount++;
            NSLog(@"我失敗了");
            [subscriber sendError:nil];
        }else{
            NSLog(@"經(jīng)歷了數(shù)百次失敗后");
            [subscriber sendNext:nil];
        }
        return nil;
    }] retry] subscribeNext:^(id x) {
        NSLog(@"終于成功了");
    }];

//Test[2411:77080] 我失敗了  
// Test[2411:77080] 我失敗了  
//.....
// Test[2411:77080] 經(jīng)歷了數(shù)百次失敗后  
// Test[2411:77080] 終于成功了

以上


下面是 MVVM 與 ReactiveCocoa 項目實例演示
摘錄翻譯自ReactiveCocoa and MVVM, an Introduction


定義MVVM


  • Model - model 在 MVVM 中沒有真正的變化. 取決于你的偏好, 你的 model 可能會或可能不會封裝一些額外的業(yè)務邏輯工作. 我更傾向于把它當做一個容納表現(xiàn)數(shù)據(jù)-模型對象信息的結構體, 并在一個單獨的管理類中維護的創(chuàng)建/管理模型的統(tǒng)一邏輯.

  • View - view 包含實際 UI 本身(不論是 UIView 代碼, storyboard 和 xib), 任何視圖特定的邏輯, 和對用戶輸入的反饋. 在 iOS 中這不僅需要 UIView 代碼和那些文件, 還包括很多需UIViewController 處理的工作.

  • View-Model - 這個術語本身會帶來困惑, 因為它混搭了兩個我們已知的術語, 但卻是完全不同的東東. 它不是傳統(tǒng)數(shù)據(jù)-模型結構中模型的意思(又來了, 只是我喜歡這個例子). 它的職責之一就是作為一個表現(xiàn)視圖顯示自身所需數(shù)據(jù)的靜態(tài)模型;但它也有收集, 解釋和轉換那些數(shù)據(jù)的責任. 這留給了 view (controller) 一個更加清晰明確的任務: 呈現(xiàn)由 view-model 提供的數(shù)據(jù).

View-Model 和 View Controller, 在一起,但獨立


  • 有一個讓用戶輸入他們姓名的 UITextField , 和一個寫著 “Go” 的 UIButton
  • 有顯示被查看的當前用戶頭像和姓名的 UIImageViewUILabel 各一個
  • 下面放著一個顯示最新回復推文的UITableView
  • 允許無限滾動

View-Model 實例

我們的 view-model 頭文件應該長這樣:

//MYTwitterLookupViewModel.h
@interface MYTwitterLookupViewModel: NSObject
 
@property (nonatomic, assign, readonly, getter=isUsernameValid) BOOL usernameValid;
@property (nonatomic, strong, readonly) NSString *userFullName;
@property (nonatomic, strong, readonly) UIImage *userAvatarImage;
@property (nonatomic, strong, readonly) NSArray *tweets;
@property (nonatomic, assign, readonly) BOOL allTweetsLoaded;
 
@property (nonatomic, strong, readwrite) NSString *username;
 
- (void) getTweetsForCurrentUsername;
- (void) loadMoreTweets;

相當直截了當?shù)奶畛? 注意到這些壯麗的 readonly 屬性了么?這個 view-model 暴漏了視圖控制器所必需的最小量信息, 視圖控制器實際上并不在乎 view-model 是如何獲得這些信息的. 現(xiàn)在我們兩者都不在乎. 僅僅假定你習慣于標準的網(wǎng)絡服務請求, 校驗, 數(shù)據(jù)操作和存儲.

視圖控制器從 view-model 獲取的數(shù)據(jù)將用來:

  • usernameValid 的值發(fā)生變化時觸發(fā) “Go” 按鈕的 enabled 屬性
  • usernameValid 等于 NO 時調整按鈕的 alpha 值為0. 5(等于 YES 時設為1. 0)
  • 更新 UILabletext 屬性為字符串 userFullName 的值
  • 更新 UIImageViewimage 屬性為 userAvatarImage 的值
  • tweets 數(shù)組中的對象設置表格視圖中的 cell (后面會提到)
  • 當滑到表格視圖底部時如果 allTweetsLoadedNO, 提供一個 顯示 “l(fā)oading” 的 cell

視圖控制器將對 view-model 起如下作用:

  • 每當 UITextField 中的文本發(fā)生變化, 更新 view-model 上僅有的 readwrite 屬性 username
  • 當 “Go” 按鈕被按下時調用 view-mode的 getTweetsForCurrentUsername 方法
  • 當?shù)竭_表格中的 “l(fā)oading” cell 時調用 view-model 上的 loadMoreTweets 方法

視圖控制器不做的事兒:

  • 發(fā)起網(wǎng)絡服務調用
  • 管理 tweets 數(shù)組
  • 判定 username 內(nèi)容是否有效
  • 將用戶的姓和名格式化為全名
  • 下載用戶頭像并轉成 UIImage(如果你習慣在 UIImageView 上使用類別從網(wǎng)絡加載圖片, 你可以暴漏 URL 而不是圖片. 這樣就給 view-model 與 UIKit 之間一個更清晰的劃分, 但我視 UIImage 為數(shù)據(jù)而非數(shù)據(jù)的確切顯示. 這些東西不是固定死的. )

進入 ReactiveCocoa



這看起來可能像是為我們應用流程文檔中的一張老舊的計算機科學圖解. 通過陳述式的編程, 我們使用了更高層次的抽象, 來讓我們實際編程更靠近我們在腦海中設計流程的方式. 我們讓電腦為我們做更多工作. 實際的代碼更加像這幅圖了.

RACSignal

RACSignal (信號)就 RAC 來說是構造單元. 它代表我們最終將要收到的信息. 當你能將未來某時刻收到的消息具體表示出來時, 你可以開始預先(陳述性)運用邏輯并構建你的信息流,而不是必須等到事件發(fā)生(命令式).

信號會為了控制通過應用的信息流而獲得所有這些異步方法(委托, 回調 block, 通知, KVO, target/action 事件觀察, 等)并將它們統(tǒng)一到一個接口下.這只是直觀理解. 不僅是這些, 因為信息會流過你的應用, 它還提供給你輕松轉換/分解/合并/過濾信息的能力.

用 ReactiveCocoa 將 view-model 與視圖控制器連接起來.

// View Controller

- (void) viewDidLoad {
    [super viewDidLoad];
 
    RAC(self.viewModel,  username) = [myTextfield rac_textSignal];
 
    RACSignal *usernameIsValidSignal = RACObserve(self.viewModel,  usernameValid);
 
    RAC(self.goButton,  alpha) = [usernameIsValidSignal
        map:  ^(NSNumber *valid) {
            return valid. boolValue ? @1 :  @0. 5;
        }];
 
    RAC(self.goButton,  enabled) = usernameIsValidSignal;
 
    RAC(self.avatarImageView,  image) = RACObserve(self.viewModel,  userAvatarImage);
    
    RAC(self.userNameLabel,  text) = RACObserve(self.viewModel,  userFullName);
 
    @weakify(self);
    [[[RACSignal merge: @[RACObserve(self.viewModel,  tweets), 
                        RACObserve(self.viewModel,  allTweetsLoaded)]]
        bufferWithTime: 0 onScheduler: [RACScheduler mainThreadScheduler]]
        subscribeNext: ^(id value) {
            @strongify(self);
            [self.tableView reloadData];
        }];
    
    [[self.goButton rac_signalForControlEvents: UIControlEventTouchUpInside]
        subscribeNext:  ^(id value) {
            @strongify(self);
            [self.viewModel getTweetsForCurrentUsername];
        }];
}
 
-(UITableViewCell*)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
    // if table section is the tweets section
    if (indexPath. section == 0) {
        MYTwitterUserCell *cell =
        [self.tableView dequeueReusableCellWithIdentifier: @"MYTwitterUserCell" forIndexPath: indexPath];
        
        // grab the cell view model from the vc view model and assign it
        cell.viewModel = self.viewModel. tweets[indexPath. row];
        return cell;
    } else {
        // else if the section is our loading cell
        MYLoadingCell *cell =
        [self.tableView dequeueReusableCellWithIdentifier: @"MYLoadingCell" forIndexPath: indexPath];
        [self.viewModel loadMoreTweets];
        return cell;
    }
}
 
 
//
// MYTwitterUserCell
//
 
// this could also be in cell init
- (void) awakeFromNib {
    [super awakeFromNib];
    
    RAC(self.avatarImageView,  image) = RACObserve(self,  viewModel. tweetAuthorAvatarImage);
    RAC(self.userNameLabel,  text) = RACObserve(self,  viewModel. tweetAuthorFullName);
    RAC(self.tweetTextLabel,  text) = RACObserve(self,  viewModel. tweetContent);
}
RAC(self.viewModel,  username) = [myTextfield rac_textSignal];

在這我們用 RAC 庫中的方法從 UITextField 拉取一個信號. 這行代碼將 view-model 上的可讀寫屬性 username 綁定到文本框上的用戶輸入的任何更新.


RACSignal *usernameIsValidSignal = RACObserve(self.viewModel,  usernameValid);

RAC(self.goButton,  alpha) = [usernameIsValidSignal
    map:  ^(NSNumber *valid) {
        return valid. boolValue ? @1 :  @0. 5;
    }];

RAC(self.goButton,  enabled) = usernameIsValidSignal;

在這我們用 RACObserve 方法在 view-model 的 usernameValid 屬性上創(chuàng)建了一個信號 usernameIsValidSignal. 無論何時屬性發(fā)生變化, 它將會沿著管道發(fā)送一個新的 @YES 或 @NO. 我們拿到那個值并將其綁定到 goButton 的兩個屬性上. 首先我們將 alpha 分別對應 YES 或 NO 更新到1或0. 5(記著在這必須返回 NSNumber). 然后我們直接將信號綁定到enabled 屬性, 因為 YES 和 NO 在這無需轉換就能完美地運作.


RAC(self.avatarImageView,  image) = RACObserve(self.viewModel,  userAvatarImage);
RAC(self.userNameLabel,  text) = RACObserve(self.viewModel,  userFullName);

下面我們?yōu)楸眍^的圖像視圖和用戶標簽創(chuàng)建綁定, 再次在 view-model 上對應的屬性上用 RACObserve 宏創(chuàng)建信號

@weakify(self);
[[[RACSignal merge: @[RACObserve(self.viewModel,  tweets), 
                     RACObserve(self.viewModel,  allTweetsLoaded)]]
    bufferWithTime: 0 onScheduler: [RACScheduler mainThreadScheduler]]
    subscribeNext: ^(id value) {
        @strongify(self);
        [self.tableView reloadData];
    }];

這貨看上去有點詭異, 所以我們在這上多花點時間. 我們想在 view-model 上 tweets 數(shù)組或 allTweetsLoaded 屬性發(fā)生變化時更新表格視圖. (在這個例子中, 我們要用一個簡單的方法來重新加載整張表. )所以我們將這兩個屬性被觀察后創(chuàng)建的兩個信號合并成一個更大的信號, 當兩個屬性中有一個發(fā)生變化, 這個信號就會發(fā)送值. (你一貫認為信號的值是同類型的, 不會像這個信號有一樣混雜的值. 這很可能在 Swift 版本的 RAC 中強制要求, 但在這我們不關心發(fā)出的真實值, 我們只是用它來觸發(fā)表格式圖的重新加載. )

那么這兒看起來最嚇人的部分可能是信號鏈中的bufferWithTime: onScheduler: 方法. 需要它來圍繞 UIKit 中的一個問題進行變通. tweetsallTweetsLoaded 這兩個屬性我們都需要追蹤, 萬一 tweets 變化和 allTweetsLoaded 為否(不管怎樣我們都得重新加載表格). 有時兩個屬性都將在同一準確的時間發(fā)生變化, 意味著合并后的大信號中的兩個信號都會發(fā)送一個值, 那么 reloadData 方法將會在同一個運行循環(huán)中被調用兩次. UIKit 不喜歡這樣. bufferWithTime: 在給明的時間內(nèi)抓取所有下一個到來的值, 當給定的時間過后將所有值合在一起發(fā)給訂閱者. 通過傳入0作為時間, bufferWithTime: 將會抓取那個合并信號在特定的運行循環(huán)中發(fā)出的全部值, 并將他們一起發(fā)送出去. (NSTimer 以同樣的方式工作, 這不是巧合, 因為 bufferWithTime: 是用 NSTimer 構建的. )暫時不用擔心 scheduler, 試把它想做指明這些值必須在主線程上被發(fā)送. 現(xiàn)在我們確保 reloadData 每次運行循環(huán)只被調用一次.

注意我在這用 @weakify/@strongify宏切換 strong 和 weak. 這在創(chuàng)建所有這些 block 時非常重要. 在 RAC 的 block 中使用 selfself 將會被捕獲為強引用并得到保留環(huán), 除非你尤其意識到要破除保留環(huán)

[[self.goButton rac_signalForControlEvents: UIControlEventTouchUpInside]
    subscribeNext:  ^(id value) {
        @strongify(self);
        [self.viewModel getTweetsForCurrentUsername];
    }];

我們已經(jīng)搞定了 cellForRowAtIndexPath 的第一部分, 那么我在這將只說下 loading cell:

MYLoadingCell *cell =
    [self.tableView dequeueReusableCellWithIdentifier: @"MYLoadingCell" forIndexPath: indexPath];
[self.viewModel loadMoreTweets];
return cell;

這是另一塊我們以后將利用到 RACCommand 的地方, 但目前我們只是調用 view-model 的 loadMoreTweets 方法. 我們將只是信任如果 cell 顯示或隱藏多次的話 view-model 會避免多次內(nèi)部調用.

- (void) awakeFromNib {
    [super awakeFromNib];

    RAC(self.avatarImageView,  image) = RACObserve(self,  viewModel. tweetAuthorAvatarImage);
    RAC(self.userNameLabel,  text) = RACObserve(self,  viewModel. tweetAuthorFullName);
    RAC(self.tweetTextLabel,  text) = RACObserve(self,  viewModel. tweetContent);
}

這段現(xiàn)在應該非常直接了, 除此之外我想指出一點. 我們正在將圖片和文字綁定到 UI 上對應的屬性, 但注意 viewModel 出現(xiàn)在 RACObserve 宏中逗號右邊. 這些 cell 終將被重用, 新的 view-models 將會被賦值. 如果我們不將 viewModel 放在逗號右邊, 那就會監(jiān)聽 viewModel 屬性的變化然后每次都要重新設置綁定;如果放在逗號右邊, RACObserve 將會為我們負責這些事兒. 因此我們只需要設定一次綁定并讓 Reactive Cocoa 做剩余的部分. 這是在綁定表格 cell 時為了性能需要記住的好東西. 我在實踐中即使是有很多表格 cell 依然沒有出過問題.


?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容