ReactiveCocoa教程:下半部【譯】

原文:ReactiveCocoa Tutorial – The Definitive Introduction: Part 2/2
上半部翻譯:ReactiveCocoa教程:上半部【譯】

ReactiveCocoa框架讓你可以在iOS應(yīng)用中使用響應(yīng)式函數(shù)編程(FRP)耘擂。在教程的上半部分你學(xué)會(huì)了如何用發(fā)送事件流的信號(hào)替換標(biāo)準(zhǔn)的動(dòng)作和事件處理邏輯冯键,還有如何對(duì)這些信號(hào)進(jìn)行轉(zhuǎn)換惊豺、拆分和重組。

而在教程的下半部分,你將學(xué)到ReactiveCocoa更深層次的功能兑宇,如:

  • 另外兩種事件類型:errorcomplete
  • 限流
  • 多線程
  • 持續(xù)化
  • 等等……

事不宜遲末盔,立馬開始吧!

推特即時(shí)搜索

在本教程中你將要開發(fā)的應(yīng)用叫做推特即時(shí)搜索(模仿谷歌即時(shí)搜索的概念)媒吗,一個(gè)在輸入時(shí)即時(shí)更新搜索記錄的推特搜索應(yīng)用。

應(yīng)用的初始項(xiàng)目包含了一些你開始時(shí)需要的基礎(chǔ)的界面和普通代碼乙埃。和教程的上半部分一樣闸英,你需要使用CocoaPods獲取ReactiveCocoa框架并整合到你的項(xiàng)目中锯岖。初始項(xiàng)目已經(jīng)包含了必須的Podfile文件,所以直接打開終端窗口和執(zhí)行下列命令:

pod install

如果正確執(zhí)行的話甫何,你會(huì)看到相似輸出如下:

Analyzing dependencies
Downloading dependencies
Using ReactiveCocoa (2.1.8)
Generating Pods project
Integrating client project

這回生成一個(gè)Xcode workspace文件:TwitterInstant.xcworkspace出吹。在Xcode中打開該文件,并確定里面包含了兩個(gè)項(xiàng)目:

  • TwitterInstant:應(yīng)用邏輯所在
  • Pods:項(xiàng)目的外部引用辙喂,現(xiàn)在包含ReactiveCocoa框架

編譯運(yùn)行趋箩,你會(huì)看到下圖的界面:


先花點(diǎn)時(shí)間熟悉一下應(yīng)用的代碼。這是一個(gè)非常簡(jiǎn)單基于拆分視圖控制器的app(split view controller-based app)加派。左邊的部分是RWSearchFormViewController叫确,包含了一些通過storyboard添加的UI事件和一個(gè)外聯(lián)的搜索文本框。右邊的部分是RWSearchResultsViewController芍锦,暫時(shí)只是一個(gè)UITableViewController的子類竹勉。

打開RWSearchFormViewController.m文件你就能看到在viewDidLoad方法中定位了結(jié)果展示控制器,并將它指向resultsViewController私有屬性娄琉。由于這個(gè)應(yīng)用最主要的邏輯就落在RWSearchFormViewController上次乓,這個(gè)屬性將有助于為RWSearchResultsViewController提供搜索的結(jié)果。

校驗(yàn)搜索文本

你首先要做的是驗(yàn)證搜索文本孽水,確保它的長(zhǎng)度大于兩個(gè)字節(jié)票腰。如果你完成了上半部教程的話,這對(duì)你來說應(yīng)該是記憶猶新女气。在RWSearchFormViewController.mviewDidLoad方法下添加以下代碼:

- (BOOL)isValidSearchText:(NSString *)text {
  return text.length > 2;
}

這方法簡(jiǎn)單地判斷搜索字符串是否長(zhǎng)于兩個(gè)字節(jié)杏慰。這邏輯簡(jiǎn)單得你可能會(huì)問:“為什么這都要單獨(dú)分離出一個(gè)方法呢?”

現(xiàn)在的邏輯的確相當(dāng)?shù)暮?jiǎn)單炼鞠,但如果將來這校驗(yàn)需要變得更為復(fù)雜呢缘滥?在上面的例子中,你只需要在改變一個(gè)地方就可以了谒主。不止如此朝扼,上面的實(shí)現(xiàn)讓你的代碼可讀性更好,指出了你判斷字符串長(zhǎng)度的原因霎肯。想必我們都遵循著良好的編碼習(xí)慣對(duì)么擎颖?

在文件的頂部導(dǎo)入ReactiveCocoa:

#import <ReactiveCocoa.h>

在同一文件的viewDidLoad方法末端添加如下代碼:

[[self.searchText.rac_textSignal
  map:^id(NSString *text) {
    return [self isValidSearchText:text] ?
      [UIColor whiteColor] : [UIColor yellowColor];
  }]
  subscribeNext:^(UIColor *color) {
    self.searchText.backgroundColor = color;
  }];

不明白這都做了什么?上面的代碼實(shí)現(xiàn)了三件事:

  • 獲取了搜索文本框的文本信號(hào)
  • 把文本是否有效的校驗(yàn)結(jié)果轉(zhuǎn)換成背景顏色
  • 然后在subscribeNext:的block中將上一步所得賦值給backgroundColor屬性

編譯運(yùn)行后能看到當(dāng)搜索文本過短時(shí)观游,文本框會(huì)判斷這為無效輸入搂捧,并把背景顏色變成黃色。

如果用圖表描述的話备典,這個(gè)簡(jiǎn)單的響應(yīng)式管道看起來是這樣的:

每當(dāng)文本發(fā)生改變時(shí)异旧,rac_textSignal就會(huì)發(fā)送包含當(dāng)前文本內(nèi)容的next事件。map方法把文本轉(zhuǎn)化成顏色提佣,然后在subscribeNext:環(huán)節(jié)中獲取并賦值給文本框的背景顏色吮蛹。

想必你還記得上半部中關(guān)于這一部分內(nèi)容對(duì)嗎?如果不記得拌屏,你可能就需要先停下來潮针,去回顧一下上半部的練習(xí)部分了。

而在添加推特的搜索邏輯前倚喂,這還有一些更加有趣的話題需要提及每篷。

格式化管道

當(dāng)你研究格式化ReactiveCocoa代碼時(shí),慣例是一個(gè)操作對(duì)應(yīng)一行端圈,并垂直對(duì)齊每一個(gè)步驟焦读。

在下圖中,你可以看到一個(gè)在更為復(fù)雜情況下的格式對(duì)齊舱权,這是從上一個(gè)教程中截取出來的:

這讓你更容易看到管道的操作組成矗晃。同時(shí)這精簡(jiǎn)了每個(gè)block中的代碼,任何超過兩行的代碼都應(yīng)該封裝為一個(gè)私有方法宴倍。

但很不幸张症,Xcode并不是太喜歡這種格式化形式,所以你可能需要手動(dòng)與它的自動(dòng)縮格邏輯作斗爭(zhēng)鸵贬!

內(nèi)存管理

考慮一下你添加到TwitterInstantapp的代碼俗他,你是否為你剛創(chuàng)建的管道是如何保持(retained)的感到疑惑?當(dāng)然了阔逼,由于管道并沒有指向一個(gè)變量或者屬性兆衅,它的引用計(jì)數(shù)自然不會(huì)增加,那它隨后是否就會(huì)被直接銷毀呢嗜浮?

匿名構(gòu)造管道是ReactiveCocoa的其中一個(gè)設(shè)計(jì)理念涯保。回顧至今為止你寫的所有響應(yīng)式代碼周伦,這應(yīng)該是顯而易見的夕春。

為了支持這種特性,ReactiveCocoa維系保持了它自己的全局信號(hào)集(global set of signals)专挪。如果信號(hào)有一個(gè)或多個(gè)訂閱者的話及志,信號(hào)就會(huì)被激活。如果所有的訂閱者都給移除了寨腔,該信號(hào)就可以被回收速侈。想知道更多關(guān)于ReactiveCocoa內(nèi)管理這個(gè)過程的內(nèi)容,你可以瀏覽Memory Management文檔(譯注:文檔已失效)迫卢。

這就剩下最后一個(gè)問題了:怎樣取消信號(hào)的訂閱呢倚搬?訂閱在接收到completedevent事件后,就會(huì)自動(dòng)移除(你很快就會(huì)學(xué)到更多關(guān)于這部分的內(nèi)容)乾蛤。而要手動(dòng)移除的話可以借助RACDisposable.

RACSignal的訂閱方法都返回了一個(gè)RACDisposable實(shí)例用以在處理方法中手動(dòng)移除訂閱每界。舉一個(gè)基于現(xiàn)有管道的簡(jiǎn)單例子:

RACSignal *backgroundColorSignal =
  [self.searchText.rac_textSignal
    map:^id(NSString *text) {
      return [self isValidSearchText:text] ?
        [UIColor whiteColor] : [UIColor yellowColor];
    }];
 
RACDisposable *subscription =
  [backgroundColorSignal
    subscribeNext:^(UIColor *color) {
      self.searchText.backgroundColor = color;
    }];
 
// at some point in the future ...
[subscription dispose];

可能在實(shí)際中你很少會(huì)這樣做捅僵,但知道這么一個(gè)可行操作還是很有價(jià)值的。

注意:相對(duì)應(yīng)的眨层,如果你創(chuàng)建了一個(gè)管道但不曾對(duì)其訂閱庙楚,這管道里的代碼,包括像doNext:這樣的副作用都永遠(yuǎn)不會(huì)執(zhí)行趴樱。

避免引用循環(huán)

ReactiveCocoa已經(jīng)在背后作了很多精妙的處理馒闷,這意味著你并不需要擔(dān)心太多關(guān)于信號(hào)內(nèi)存管理的細(xì)節(jié)。但這還是有一個(gè)重要的內(nèi)存相關(guān)問題你需要關(guān)心的叁征。

看看你剛才添加的響應(yīng)式代碼:

[[self.searchText.rac_textSignal
  map:^id(NSString *text) {
    return [self isValidSearchText:text] ?
      [UIColor whiteColor] : [UIColor yellowColor];
  }]
  subscribeNext:^(UIColor *color) {
    self.searchText.backgroundColor = color;
  }];

subscribeNext:的block中使用self以獲取文本框的引用纳账。block從會(huì)從封閉作用域中捕獲并持有了相關(guān)值,因而當(dāng)self和信號(hào)中存在強(qiáng)引用時(shí)捺疼,就會(huì)導(dǎo)致引用循環(huán)疏虫。這會(huì)不會(huì)導(dǎo)致問題取決于self對(duì)象的的生命周期。如果像這個(gè)例子一樣帅涂,它的生命周期貫穿整個(gè)應(yīng)用议薪,就并不構(gòu)成問題。但這在更加復(fù)雜的應(yīng)用中是很少出現(xiàn)的媳友。

為了避免潛在的引用循環(huán)斯议,蘋果的官方文檔Working With Blocks推薦對(duì)self使用弱引用。你可以在現(xiàn)有的代碼中作如下實(shí)現(xiàn):

__weak RWSearchFormViewController *bself = self; // Capture the weak reference
 
[[self.searchText.rac_textSignal
  map:^id(NSString *text) {
    return [self isValidSearchText:text] ?
      [UIColor whiteColor] : [UIColor yellowColor];
  }]
  subscribeNext:^(UIColor *color) {
    bself.searchText.backgroundColor = color;
  }];

上面的代碼中bself是對(duì)self的引用醇锚,__weak標(biāo)記讓該引用變?yōu)槿跻煤哂W⒁?code>subscribeNext:的block中現(xiàn)在使用的就是bself變量了,這看起來實(shí)在相當(dāng)不美觀焊唬!

ReactiveCocoa框架提供了一個(gè)可以代替上面代碼的小竅門恋昼。在文件頂端添加導(dǎo)入如下:

#import "RACEXTScope.h"

然后替換剛才的代碼如下:

@weakify(self)
[[self.searchText.rac_textSignal
  map:^id(NSString *text) {
    return [self isValidSearchText:text] ?
      [UIColor whiteColor] : [UIColor yellowColor];
  }]
  subscribeNext:^(UIColor *color) {
    @strongify(self)
    self.searchText.backgroundColor = color;
  }];

代碼中的@weakify@strongify是定義在Extended Objective-C庫的宏,這也已經(jīng)包含在ReactiveCocoa框架中赶促。@weakify宏創(chuàng)建了弱應(yīng)用的影子變量(shadow variables)(如果你需要多個(gè)弱引用液肌,你可以傳入多個(gè)變量),@strongify宏則使用先前傳到@weakify的變量創(chuàng)建強(qiáng)引用鸥滨。

注意:如果你對(duì)@weakify@strongify的具體操作感到好奇嗦哆,你可以在Xcode中選擇Product -> Perform Action -> Preprocess “RWSearchForViewController”。這會(huì)對(duì)視圖控制器進(jìn)行預(yù)處理婿滓,展開所有的宏讓你看到最終的輸出老速。

最后需要注意的是,在block中使用實(shí)例變量時(shí)也要小心凸主。這也會(huì)導(dǎo)致block對(duì)self進(jìn)行強(qiáng)引用橘券。你可以打開編譯器警告,當(dāng)你的代碼導(dǎo)致這種問題時(shí)去提醒你。在項(xiàng)目的build settings中搜索retain旁舰,找到如下的設(shè)置:


好了锋华,恭喜你終于熬過了理論知識(shí)!現(xiàn)在你已經(jīng)為最有趣的部分做好了充足的準(zhǔn)備:為應(yīng)用添加真正的功能鬓梅!

注意:看過上一個(gè)教程的敏銳讀者想必已經(jīng)發(fā)現(xiàn)在這管道中可以使用RAC宏去替代subscribeNext:供置。如果你已經(jīng)發(fā)現(xiàn)了谨湘,那就改改上面的代碼并獎(jiǎng)勵(lì)自己一朵小紅花吧绽快!

連接推特

你將要使用Social Framework在你的應(yīng)用中搜索推特,使用Accounts Framework去獲取推特的授權(quán)紧阔。想要獲取更多關(guān)于Social Framework的信息坊罢,可以查看iOS 6 by Tutorials這篇介紹這個(gè)框架的文章。

在添加代碼前擅耽,你需要在運(yùn)行本應(yīng)用的模擬器或iPad上輸入你推特的用戶密碼活孩。打開設(shè)置并選中推特選項(xiàng),在屏幕的右方添加你的用戶密碼:


初始項(xiàng)目已經(jīng)添加了所需框架乖仇,所以你只需要導(dǎo)入相關(guān)頭文件憾儒。在RWSearchFormViewController.m的頂端添加引用如下:

#import <Accounts/Accounts.h>
#import <Social/Social.h>

在引用的下方添加如下枚舉和常量:

typedef NS_ENUM(NSInteger, RWTwitterInstantError) {
    RWTwitterInstantErrorAccessDenied,
    RWTwitterInstantErrorNoTwitterAccounts,
    RWTwitterInstantErrorInvalidResponse
};
 
static NSString * const RWTwitterInstantDomain = @"TwitterInstant";

你很快就會(huì)用到這些去區(qū)分錯(cuò)誤。

在同一個(gè)文件乃沙,在現(xiàn)有的屬性聲明下添加如下屬性:

@property (strong, nonatomic) ACAccountStore *accountStore;
@property (strong, nonatomic) ACAccountType *twitterAccountType;

ACAccountsStore類為你的設(shè)備提供了多種可用的社交媒體賬號(hào)的連接途徑起趾,ACAccountType則類代表了賬戶的具體類型。

在同一文件的viewDidLoad方法末端添加代碼如下:

self.accountStore = [[ACAccountStore alloc] init];
self.twitterAccountType = [self.accountStore 
  accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];

以上代碼創(chuàng)建了賬號(hào)的庫(accounts store)和推特的賬號(hào)標(biāo)識(shí)警儒。

當(dāng)應(yīng)用請(qǐng)求連接一個(gè)社交賬戶時(shí)训裆,用戶會(huì)看到一個(gè)彈窗。這是一個(gè)異步操作蜀铲,所以最好將它用信號(hào)封裝起來边琉,以便響應(yīng)式使用。

繼續(xù)添加以下代碼:

- (RACSignal *)requestAccessToTwitterSignal {
 
  // 1 - define an error
  NSError *accessError = [NSError errorWithDomain:RWTwitterInstantDomain
                                             code:RWTwitterInstantErrorAccessDenied
                                         userInfo:nil];
 
  // 2 - create the signal
  @weakify(self)
  return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    // 3 - request access to twitter
    @strongify(self)
    [self.accountStore
       requestAccessToAccountsWithType:self.twitterAccountType
         options:nil
      completion:^(BOOL granted, NSError *error) {
          // 4 - handle the response
          if (!granted) {
            [subscriber sendError:accessError];
          } else {
            [subscriber sendNext:nil];
            [subscriber sendCompleted];
          }
        }];
    return nil;
  }];
}

該方法做了以下操作:

  1. 定義了一個(gè)錯(cuò)誤记劝,當(dāng)用戶連接遭拒時(shí)發(fā)送变姨。
  2. 像第一篇文章所說,類方法createSignal返回了一個(gè)RACSignal的實(shí)例厌丑。
  3. 通過賬戶庫鏈接到推特定欧。此時(shí),用戶會(huì)看到是否允許app連接到他們推特賬號(hào)的提示蹄衷。
  4. 當(dāng)用戶同意或拒絕了連接忧额,信號(hào)事件就會(huì)發(fā)送。如果用戶同意連接愧口,一個(gè)next事件和緊接一個(gè)completed事件就會(huì)被發(fā)送睦番。如果用戶拒絕了連接一個(gè)error事件就會(huì)被發(fā)送。

回想一下上半部的教程,一個(gè)信號(hào)可以發(fā)送三種不同類型的事件:

  • Next
  • Completed
  • Error

在信號(hào)的整個(gè)生命周期托嚣,它可能不發(fā)送任何事件巩检,也可能發(fā)送一個(gè)或多個(gè)next事件然后緊跟一個(gè)completed事件或者error事件。

最后為了使用這個(gè)信號(hào)示启,在viewDidLoad方法末端添加以下代碼:

[[self requestAccessToTwitterSignal]
  subscribeNext:^(id x) {
    NSLog(@"Access granted");
  } error:^(NSError *error) {
    NSLog(@"An error occurred: %@", error);
  }];

編譯運(yùn)行兢哭,你就能看到下圖這樣的彈出框:


如果你點(diǎn)擊確定按鈕,控制臺(tái)就會(huì)答應(yīng)subscribeNext:block中的記錄信息夫嗓,相反迟螺,如果你點(diǎn)擊不允許,error里的代碼塊就會(huì)執(zhí)行并打印響應(yīng)的記錄舍咖。

賬戶管理框架會(huì)記住你的選擇矩父。所以為了測(cè)試兩種情況,你需要在菜單中選擇重置模擬器: iOS Simulator -> Reset Contents and Settings …排霉。這會(huì)有一點(diǎn)繁瑣窍株,因?yàn)橹刂煤竽氵€需要重新輸入你的推特賬號(hào)密碼!

鏈接信號(hào)

當(dāng)用戶成功連接到他們的推特賬號(hào)(希望如此!),應(yīng)用就要繼續(xù)監(jiān)聽搜索輸入框的改變來搜索推特歇式。

應(yīng)用需要等連接推特的信號(hào)發(fā)送completed事件户誓,并傳遞給輸入框的信號(hào)。這種連續(xù)的信號(hào)鏈接是相當(dāng)常見的問題,但是ReactiveCocoa對(duì)此有非常優(yōu)雅的解決方案。

替換viewDidLoad末端現(xiàn)有的管道如下:

[[[self requestAccessToTwitterSignal]
  then:^RACSignal *{
    @strongify(self)
    return self.searchText.rac_textSignal;
  }]
  subscribeNext:^(id x) {
    NSLog(@"%@", x);
  } error:^(NSError *error) {
    NSLog(@"An error occurred: %@", error);
  }];

then方法會(huì)一直等待直到信號(hào)的completed事件被發(fā)送,然后轉(zhuǎn)訂閱參數(shù)代碼塊中返回的信號(hào)旦部。這有效的將控制權(quán)從一個(gè)信號(hào)轉(zhuǎn)遞給下一個(gè)信號(hào)。

注意:你已經(jīng)在上一個(gè)管道弱引用過self较店,所以不再需要在這個(gè)管道前添加@weakify(self)了士八。

編譯運(yùn)行并允許連接,你會(huì)看到你在搜索文本框輸入的文本此時(shí)打印在了控制臺(tái):

2014-01-04 08:16:11.444 TwitterInstant[39118:a0b] m
2014-01-04 08:16:12.276 TwitterInstant[39118:a0b] ma
2014-01-04 08:16:12.413 TwitterInstant[39118:a0b] mag
2014-01-04 08:16:12.548 TwitterInstant[39118:a0b] magi
2014-01-04 08:16:12.628 TwitterInstant[39118:a0b] magic
2014-01-04 08:16:13.172 TwitterInstant[39118:a0b] magic!

接下來梁呈,為管道添加一個(gè)過濾操作婚度,將無效的搜索字符串移除掉。在這個(gè)例子中官卡,無效指的就是少于3個(gè)字節(jié)的字符串:

[[[[self requestAccessToTwitterSignal]
  then:^RACSignal *{
    @strongify(self)
    return self.searchText.rac_textSignal;
  }]
  filter:^BOOL(NSString *text) {
    @strongify(self)
    return [self isValidSearchText:text];
  }]
  subscribeNext:^(id x) {
    NSLog(@"%@", x);
  } error:^(NSError *error) {
    NSLog(@"An error occurred: %@", error);
  }];

再次編譯運(yùn)行蝗茁,實(shí)際觀察一下過濾效果:

2014-01-04 08:16:12.548 TwitterInstant[39118:a0b] magi
2014-01-04 08:16:12.628 TwitterInstant[39118:a0b] magic
2014-01-04 08:16:13.172 TwitterInstant[39118:a0b] magic!

用圖表說明一下現(xiàn)在的應(yīng)用邏輯,那看起來是這樣的:


應(yīng)用管道從requestAccessToTwitterSignal開始寻咒,然后切換到rac_textSignal哮翘。與此同時(shí),next事件通過過濾器最終到達(dá)訂閱的block毛秘。第一個(gè)環(huán)節(jié)中發(fā)送的error事件同樣能夠被同一個(gè)subscribeNext:error:方法捕獲到饭寺。

現(xiàn)在你已經(jīng)有了一個(gè)發(fā)送搜索文本的信號(hào)阻课,是時(shí)候使用來搜索推特了!你現(xiàn)在享受到其中的樂趣了么艰匙?想必是限煞,畢竟你已經(jīng)大展一番拳腳了!

推特搜索

Social Framework是使用推特搜索API的一種方式员凝。但是署驻,如你所想,Social Framework并不是響應(yīng)式的健霹!下一步要做的就是將需要的API方法封裝在一個(gè)信號(hào)中調(diào)用旺上。 你這下應(yīng)該搞明白了吧!
RWSearchFormViewController.m中添加下列方法:

- (SLRequest *)requestforTwitterSearchWithText:(NSString *)text {
  NSURL *url = [NSURL URLWithString:@"https://api.twitter.com/1.1/search/tweets.json"];
  NSDictionary *params = @{@"q" : text};
 
  SLRequest *request =  [SLRequest requestForServiceType:SLServiceTypeTwitter
                                           requestMethod:SLRequestMethodGET
                                                     URL:url
                                              parameters:params];
  return request;
}

這根據(jù)v1.1 REST API標(biāo)準(zhǔn)創(chuàng)建了搜索推特的請(qǐng)求。上面的代碼使用q搜索參數(shù)用以搜索所有包含搜索關(guān)鍵字的推特骤公。你可以在推特的接口文檔查看更多關(guān)于這個(gè)搜索接口信息抚官,以及其他可以傳遞的有效參數(shù)列表扬跋。

下一步就是基于這個(gè)請(qǐng)求創(chuàng)建信號(hào)阶捆。在同一文件添加下列方法:

- (RACSignal *)signalForSearchWithText:(NSString *)text {
 
  // 1 - define the errors
  NSError *noAccountsError = [NSError errorWithDomain:RWTwitterInstantDomain
                                                 code:RWTwitterInstantErrorNoTwitterAccounts
                                             userInfo:nil];
 
  NSError *invalidResponseError = [NSError errorWithDomain:RWTwitterInstantDomain
                                                      code:RWTwitterInstantErrorInvalidResponse
                                                  userInfo:nil];
 
  // 2 - create the signal block
  @weakify(self)
  return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    @strongify(self);
 
    // 3 - create the request
    SLRequest *request = [self requestforTwitterSearchWithText:text];
 
    // 4 - supply a twitter account
    NSArray *twitterAccounts = [self.accountStore
      accountsWithAccountType:self.twitterAccountType];
    if (twitterAccounts.count == 0) {
      [subscriber sendError:noAccountsError];
    } else {
      [request setAccount:[twitterAccounts lastObject]];
 
      // 5 - perform the request
      [request performRequestWithHandler: ^(NSData *responseData,
                                          NSHTTPURLResponse *urlResponse, NSError *error) {
        if (urlResponse.statusCode == 200) {
 
          // 6 - on success, parse the response
          NSDictionary *timelineData =
             [NSJSONSerialization JSONObjectWithData:responseData
                                             options:NSJSONReadingAllowFragments
                                               error:nil];
          [subscriber sendNext:timelineData];
          [subscriber sendCompleted];
        }
        else {
          // 7 - send an error on failure
          [subscriber sendError:invalidResponseError];
        }
      }];
    }
 
    return nil;
  }];
}

分解一下步驟:

  1. 剛開始,定義了兩種不同的錯(cuò)誤钦听,一個(gè)表示用戶尚未在設(shè)備中添加推特賬戶洒试,另一個(gè)表示查詢過程中發(fā)生的錯(cuò)誤。
  2. 像之前一樣朴上,創(chuàng)建一個(gè)信號(hào)垒棋。
  3. 使用上一步你創(chuàng)建的方法根據(jù)提供的搜索關(guān)鍵字創(chuàng)建請(qǐng)求。
  4. 查詢賬號(hào)庫中第一個(gè)有效的推特賬戶痪宰。如果沒有任何賬戶返回叼架,發(fā)送錯(cuò)誤事件。
  5. 執(zhí)行請(qǐng)求衣撬。
  6. 當(dāng)成功返回時(shí)(HTTP返回編碼為200)乖订,轉(zhuǎn)換返回的JSON數(shù)據(jù)并伴隨next事件發(fā)送,緊跟發(fā)送一個(gè)completed事件具练。
  7. 當(dāng)返回狀態(tài)為不成功時(shí)乍构,發(fā)送一個(gè)error事件。

現(xiàn)在就能使用這個(gè)新的信號(hào)了扛点!

在本教程的上半部分你學(xué)會(huì)了如何使用flattenMap去映射每一個(gè)next事件為一個(gè)全新的信號(hào)并接著訂閱它「缯冢現(xiàn)在就要再次運(yùn)用這個(gè)方法。更新viewDidLoad方法內(nèi)末端的管道陵究,在最后添加flattenMap環(huán)節(jié):

[[[[[self requestAccessToTwitterSignal]
  then:^RACSignal *{
    @strongify(self)
    return self.searchText.rac_textSignal;
  }]
  filter:^BOOL(NSString *text) {
    @strongify(self)
    return [self isValidSearchText:text];
  }]
  flattenMap:^RACStream *(NSString *text) {
    @strongify(self)
    return [self signalForSearchWithText:text];
  }]
  subscribeNext:^(id x) {
    NSLog(@"%@", x);
  } error:^(NSError *error) {
    NSLog(@"An error occurred: %@", error);
  }];

編譯運(yùn)行眠饮,在搜索框中輸入一些文本。當(dāng)文本達(dá)到或超過3個(gè)字節(jié)時(shí)铜邮,你就能在控制臺(tái)中看到推特的搜索記錄仪召。
下面節(jié)選了一段你會(huì)看到的數(shù)據(jù)樣式:

2014-01-05 07:42:27.697 TwitterInstant[40308:5403] {
    "search_metadata" =     {
        "completed_in" = "0.019";
        count = 15;
        "max_id" = 419735546840117248;
        "max_id_str" = 419735546840117248;
        "next_results" = "?max_id=419734921599787007&q=asd&include_entities=1";
        query = asd;
        "refresh_url" = "?since_id=419735546840117248&q=asd&include_entities=1";
        "since_id" = 0;
        "since_id_str" = 0;
    };
    statuses =     (
                {
            contributors = "<null>";
            coordinates = "<null>";
            "created_at" = "Sun Jan 05 07:42:07 +0000 2014";
            entities =             {
                hashtags = ...

signalForSearchText:方法發(fā)送的error事件同樣能給subscribeNext:error:接收到。你可能已經(jīng)記住了這點(diǎn),但相比你更可能希望親手試驗(yàn)一下返咱!

在模擬器中打開設(shè)置并選中的的推特賬戶钥庇,然后點(diǎn)擊刪除賬戶按鈕:


重運(yùn)行應(yīng)用,應(yīng)用仍然會(huì)獲取用戶推特賬號(hào)的授權(quán)咖摹,盡管已經(jīng)現(xiàn)在已經(jīng)沒有有效的賬號(hào)了评姨。因此signalForSearchText方法會(huì)發(fā)送一個(gè)錯(cuò)誤,在控制臺(tái)打佑┣纭:

2014-01-05 07:52:11.705 TwitterInstant[41374:1403] An error occurred: Error 
  Domain=TwitterInstant Code=1 "The operation couldn’t be completed. (TwitterInstant error 1.)"

Code=1指明了這是一個(gè)RWTwitterInstantErrorNoTwitterAccounts錯(cuò)誤吐句。在生產(chǎn)環(huán)境的應(yīng)用中,你會(huì)希望把錯(cuò)誤編碼轉(zhuǎn)換成更有意義的形式店读,而不是只在控制臺(tái)打印記錄嗦枢。

這引出了一個(gè)關(guān)于error事件的要點(diǎn);當(dāng)信號(hào)發(fā)出一個(gè)錯(cuò)誤時(shí)屯断,它會(huì)直接傳遞給處理錯(cuò)誤的block文虏。這是一個(gè)異常的處理流。

提醒:想試驗(yàn)訪問推特失敗時(shí)的異常處理流的話殖演,有一個(gè)小竅門氧秘,把請(qǐng)求入?yún)⒏臑闊o效數(shù)據(jù)就可以了!

多線程

相信你已經(jīng)迫不及待要將搜索結(jié)果的JSON輸出轉(zhuǎn)化為UI了趴久,但在那之前你還需要做一件事丸相。而為了明確你要做的事,你還需要做一點(diǎn)探索彼棍。

subscribeNext:error:方法中如下的位置添加斷點(diǎn):

重運(yùn)行應(yīng)用灭忠,如果有必要的話重新輸入你的推特賬號(hào)密碼,然后在搜索框中輸入一些內(nèi)容座硕。當(dāng)執(zhí)行到斷點(diǎn)時(shí)你會(huì)看到下圖相似的景象:


注意調(diào)試中的代碼并不是在主線程Thread 1中執(zhí)行的弛作。謹(jǐn)記你只能在主線程中更新UI界面;所以如果你希望在UI界面中更新UI的話坎吻,你需要切換執(zhí)行線程缆蝉。

這體現(xiàn)了ReactiveCocoa框架一個(gè)非常重要的特點(diǎn)。上面的操作是在信號(hào)開始發(fā)送信號(hào)的線程中執(zhí)行的瘦真。在管道的其他環(huán)節(jié)添加斷點(diǎn)?刊头,你可能會(huì)驚訝地發(fā)現(xiàn)他們并不在同一個(gè)線程中執(zhí)行!

所以要如何更新UI界面呢诸尽?傳統(tǒng)的做法是使用操作隊(duì)列(更多的細(xì)節(jié)可以看本站的另一篇文章 How To Use NSOperations and NSOperationQueues)原杂,但是ReactiveCocoa提供了一個(gè)更加簡(jiǎn)便的解決方法。

在管道的flattenMap:方法后添加deliverOn:方法如下:

[[[[[[self requestAccessToTwitterSignal]
  then:^RACSignal *{
    @strongify(self)
    return self.searchText.rac_textSignal;
  }]
  filter:^BOOL(NSString *text) {
    @strongify(self)
    return [self isValidSearchText:text];
  }]
  flattenMap:^RACStream *(NSString *text) {
    @strongify(self)
    return [self signalForSearchWithText:text];
  }]
  deliverOn:[RACScheduler mainThreadScheduler]]
  subscribeNext:^(id x) {
    NSLog(@"%@", x);
  } error:^(NSError *error) {
    NSLog(@"An error occurred: %@", error);
  }];

現(xiàn)在重運(yùn)行app您机,任意輸入一點(diǎn)內(nèi)容讓app運(yùn)行到斷點(diǎn)處穿肄。你會(huì)看到subscribeNext:error:中控制臺(tái)打印的代碼現(xiàn)在在主線程中執(zhí)行了:


什么年局?這不過簡(jiǎn)單的調(diào)整了一下代碼就能改變事件流的執(zhí)行線程?這實(shí)在太棒了咸产!你可以更新你的UI界面了矢否!

注意:如果你關(guān)注一下RACScheduler類你就能看到其提供了相當(dāng)多的選擇來實(shí)現(xiàn)不同的線程優(yōu)先級(jí)和管道延遲處理。

現(xiàn)在是時(shí)候展現(xiàn)這些推特了脑溢!

更新UI界面

打開RWSearchResultsViewController.h你能看到里面已經(jīng)定義displayTweets:了方法僵朗,用以為右手邊的視圖控制器渲染提供的推特列表。里面的實(shí)現(xiàn)非常簡(jiǎn)單屑彻,只是標(biāo)準(zhǔn)的UITableView數(shù)據(jù)源處理验庙。displayTweets:方法的唯一入?yún)⑹且粋€(gè)裝載RWTweet實(shí)例的NSArray。初始項(xiàng)目也已經(jīng)為你提供了RWTweet對(duì)象模型社牲。

subscibeNext:error:中接收的是在signalForSearchWithText:方法中從JSON轉(zhuǎn)換成的NSDictionary類型數(shù)據(jù)粪薛。所以你怎樣才能知道字典中的內(nèi)容呢?

閱讀推特的接口文檔你能看到接口的響應(yīng)示例搏恤。所得的NSDictionary與這個(gè)結(jié)構(gòu)相似违寿,里面有個(gè)叫statuses的鍵對(duì)應(yīng)值為裝載推特的NSArray,推特?cái)?shù)據(jù)也是NSDictionary類型挑社。

RWTweet已經(jīng)包含一個(gè)類方法tweetWithStatus:陨界,用以從給定格式的NSDictionary中提取數(shù)據(jù)。所以你需要做的只是編寫一個(gè)循環(huán)痛阻,并遍歷整個(gè)數(shù)組,為每條推特創(chuàng)建一個(gè)RWTweet的實(shí)例腮敌。

但是阱当,別這樣做。之后又更好的解決方法呢糜工。

這篇文章是關(guān)于ReactiveCocoa和函數(shù)式編程的弊添。數(shù)據(jù)轉(zhuǎn)換時(shí)使用函數(shù)式的接口會(huì)顯得更加干練。你可以使用LinqToObjectiveC來完成這個(gè)任務(wù)捌木。

關(guān)閉項(xiàng)目油坝,并打開你在第一個(gè)教程中使用TextEdit創(chuàng)建的Podfile文件(譯注:這里作者混亂了,指的是本項(xiàng)目中的Podfile文件刨裆,下載時(shí)已經(jīng)提供的澈圈,也并不是上一教程里創(chuàng)建的)。更新文件帆啃,添加新的依賴:

platform :ios, '7.0'
 
pod 'ReactiveCocoa', '2.1.8'
pod 'LinqToObjectiveC', '2.0.0'

打開終端并跳轉(zhuǎn)到此文件夾瞬女,執(zhí)行以下命令:

pod update

你會(huì)看到和以下相似的輸出:

Analyzing dependencies
Downloading dependencies
Installing LinqToObjectiveC (2.0.0)
Using ReactiveCocoa (2.1.8)
Generating Pods project
Integrating client project

重新打開workspace文件并確認(rèn)新的框架已經(jīng)如下圖一樣成功引入:


打開RWSearchFormViewController.m并在文件頂端添加引用如下:

#import "RWTweet.h"
#import "NSArray+LinqExtensions.h"

NSArray+LinqExtensions.h頭文件是LinqToObjectiveC的一部分,這為NSArray添加了很多方法努潘,用流式接口實(shí)現(xiàn)轉(zhuǎn)換诽偷,排序坤学,分組和過濾數(shù)據(jù)。

現(xiàn)在就立即使用這些API……更新在viewDidLoad方法末端的管道如下:

[[[[[[self requestAccessToTwitterSignal]
  then:^RACSignal *{
    @strongify(self)
    return self.searchText.rac_textSignal;
  }]
  filter:^BOOL(NSString *text) {
    @strongify(self)
    return [self isValidSearchText:text];
  }]
  flattenMap:^RACStream *(NSString *text) {
    @strongify(self)
    return [self signalForSearchWithText:text];
  }]
  deliverOn:[RACScheduler mainThreadScheduler]]
  subscribeNext:^(NSDictionary *jsonSearchResult) {
    NSArray *statuses = jsonSearchResult[@"statuses"];
    NSArray *tweets = [statuses linq_select:^id(id tweet) {
      return [RWTweet tweetWithStatus:tweet];
    }];
    [self.resultsViewController displayTweets:tweets];
  } error:^(NSError *error) {
    NSLog(@"An error occurred: %@", error);
  }];

如你所見报慕,subscribeNext:的block首先獲取了推特的NSArray深浮。linq_select方法將裝有NSDictionary實(shí)例的數(shù)組通過提供的block處理轉(zhuǎn)換成新的數(shù)組元素,最后返回一個(gè)裝有RWTweet實(shí)例的數(shù)組眠冈。

轉(zhuǎn)換成功后略号,相關(guān)推特就會(huì)被發(fā)送到結(jié)果視圖控制器。最后編譯運(yùn)行洋闽,你就能看到推特展示在UI界面中:


注意:ReactiveCocoa和LinqToObjectiveC有著相似的靈感來源玄柠。ReactiveCocoa是模仿微軟的Reactive Extensions框架,LinqToObjectiveC則是模仿它們的語言集成查詢接口(Language Integrated Query APIs)诫舅,或稱作LINQ羽利,特別是用于對(duì)象的LINQ

異步加載圖片

你可能已經(jīng)發(fā)現(xiàn)在每一條推特的左邊有一塊間隙刊懈。那個(gè)位置是用來展示推特用戶的頭像的这弧。

RWTweet類已經(jīng)有了一個(gè)profileImageUrl屬性以記錄獲取這張圖片的URL。為了使列表平滑地滾動(dòng)虚汛,你需要確保從提供的URL中獲取圖片的代碼不在主線程中執(zhí)行匾浪。這可以使用Grand Central Dispatch(GCD)或者NSOperationQueue實(shí)現(xiàn)。但是為什么不直接使用ReactiveCocoa呢卷哩?

打開RWSearchResultsViewController.m并在文件最后添加如下方法:

-(RACSignal *)signalForLoadingImage:(NSString *)imageUrl {
 
  RACScheduler *scheduler = [RACScheduler
                         schedulerWithPriority:RACSchedulerPriorityBackground];
 
  return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]];
    UIImage *image = [UIImage imageWithData:data];
    [subscriber sendNext:image];
    [subscriber sendCompleted];
    return nil;
  }] subscribeOn:scheduler];
 
}

你現(xiàn)在應(yīng)該相當(dāng)熟悉這種模式了蛋辈!

由于你希望這個(gè)信號(hào)不在主線程中執(zhí)行,上面的方法先獲取了一個(gè)后臺(tái)調(diào)度器将谊。然后創(chuàng)建一個(gè)信號(hào)冷溶,該信號(hào)在有訂閱者時(shí)下載圖像數(shù)據(jù)并生成UIImage。最后一步就是使用subscribeOn:尊浓,以保證信號(hào)在提供的調(diào)度器中執(zhí)行逞频。

搞定!

現(xiàn)在栋齿,在同一個(gè)文件中更新tableView:cellForRowAtIndex:方法苗胀,在方法返回前添加以下代碼:

cell.twitterAvatarView.image = nil;
 
[[[self signalForLoadingImage:tweet.profileImageUrl]
  deliverOn:[RACScheduler mainThreadScheduler]]
  subscribeNext:^(UIImage *image) {
   cell.twitterAvatarView.image = image;
  }];

在上面的代碼中,由于這些單元(cell)會(huì)重復(fù)使用并可能包含先前遺留的數(shù)據(jù)瓦堵,因而首先重置了圖片基协。然后創(chuàng)建用以獲取圖片數(shù)據(jù)的信號(hào)。而接下來你先前遇到過的deliverOn:方法將next事件調(diào)整到了主線程上谷丸,以便安全地執(zhí)行subscribeNext:中的block堡掏。

多么簡(jiǎn)單有效!

編譯運(yùn)行后就能看到頭像現(xiàn)在都已經(jīng)正確顯示了:


節(jié)流

你可能已經(jīng)發(fā)現(xiàn)刨疼,每當(dāng)你輸入一個(gè)新的字符泉唁,就會(huì)立馬執(zhí)行一次新的推特搜索鹅龄。如果你是一個(gè)熟練的打字員(或者只是按緊刪格鍵),這會(huì)導(dǎo)致應(yīng)用在一秒內(nèi)作出多個(gè)搜索請(qǐng)求亭畜。這種實(shí)現(xiàn)并不理想扮休,原因有二:第一,這在對(duì)推特的搜索接口造成沖擊的同時(shí)舍棄了大部分返回的結(jié)果拴鸵;第二玷坠,不斷的更新結(jié)果會(huì)擾亂用戶的注意力!

更好的實(shí)現(xiàn)應(yīng)該是當(dāng)搜索文本在一個(gè)短時(shí)間內(nèi)劲藐,比如說500毫秒八堡,沒有改變的話再執(zhí)行搜索。正如你可能猜到的那樣聘芜,ReactiveCocoa還是很容易就能實(shí)現(xiàn)這一點(diǎn)兄渺!

打開RWSearchFormViewController.m,更新viewDidLoad末端的管道汰现,在過濾后新增節(jié)流操作:

[[[[[[[self requestAccessToTwitterSignal]
  then:^RACSignal *{
    @strongify(self)
    return self.searchText.rac_textSignal;
  }]
  filter:^BOOL(NSString *text) {
    @strongify(self)
    return [self isValidSearchText:text];
  }]
  throttle:0.5]
  flattenMap:^RACStream *(NSString *text) {
    @strongify(self)
    return [self signalForSearchWithText:text];
  }]
  deliverOn:[RACScheduler mainThreadScheduler]]
  subscribeNext:^(NSDictionary *jsonSearchResult) {
    NSArray *statuses = jsonSearchResult[@"statuses"];
    NSArray *tweets = [statuses linq_select:^id(id tweet) {
      return [RWTweet tweetWithStatus:tweet];
    }];
    [self.resultsViewController displayTweets:tweets];
  } error:^(NSError *error) {
    NSLog(@"An error occurred: %@", error);
  }];

throttle操作只有在時(shí)間間隔內(nèi)沒有接收到新的next事件時(shí)才會(huì)發(fā)送next事件給下一環(huán)節(jié)挂谍。這是不是相當(dāng)簡(jiǎn)單!

編譯運(yùn)行瞎饲,這時(shí)搜索結(jié)果只在停止輸入超過500毫秒時(shí)才會(huì)更新口叙。這感覺好多了對(duì)嗎?你的用戶也會(huì)這么想的嗅战。

并且……隨著最后一步的完成妄田,你的推特即時(shí)搜索應(yīng)用已經(jīng)完成了。給自己一點(diǎn)掌聲并跳支舞放松一下吧仗哨!

如果你在教程的過程中感到迷惑的話形庭,你可以下載瀏覽最終的項(xiàng)目(當(dāng)然別忘了在打開前在項(xiàng)目所在目錄運(yùn)行pod instal命令),你也可以在GitHub找到這個(gè)項(xiàng)目厌漂,那里有對(duì)應(yīng)教程中每一步操作的提交記錄。

總結(jié)

在結(jié)束教程并給自己泡上一杯咖啡慶祝之前斟珊,非常值得欣賞一下項(xiàng)目最終搭建的管道苇倡。

這是一個(gè)相當(dāng)復(fù)雜的數(shù)據(jù)流,但所有都簡(jiǎn)明的表達(dá)在了一個(gè)響應(yīng)式管道中囤踩。這是多么迷人的景象爸冀贰!你可以想象如果使用非響應(yīng)式技術(shù)的話來時(shí)實(shí)現(xiàn)這些功能的話堵漱,應(yīng)用該變得多么復(fù)雜嗎综慎?而且要理清楚數(shù)據(jù)的流向?qū)⒆兊枚嗝蠢щy?聽著就覺得夠麻煩的了勤庐,而你現(xiàn)在已經(jīng)不再需要重蹈覆轍了示惊!

現(xiàn)在你體會(huì)ReactiveCocoa是多么了不起了吧好港!

最后一點(diǎn),ReactiveCocoa讓使用又稱為MVVM的Model View ViewModel設(shè)計(jì)模式變?yōu)榭赡苊追#涓行У姆蛛x了應(yīng)用邏輯和視圖邏輯钧汹。如果有人對(duì)后續(xù)關(guān)于用ReactiveCocoa實(shí)現(xiàn)MVVM的文章感興趣的話,請(qǐng)?jiān)谠u(píng)論中告訴我录择。我非常希望聽到能夠你的想法和經(jīng)驗(yàn)0卫场(譯注:后續(xù)也有作者關(guān)于MVVM的教程,有時(shí)間會(huì)繼續(xù)進(jìn)行翻譯0摺)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末塘秦,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子动看,更是在濱河造成了極大的恐慌尊剔,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件弧圆,死亡現(xiàn)場(chǎng)離奇詭異赋兵,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)搔预,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門霹期,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人拯田,你說我怎么就攤上這事历造。” “怎么了船庇?”我有些...
    開封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵吭产,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我鸭轮,道長(zhǎng)臣淤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任窃爷,我火速辦了婚禮邑蒋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘按厘。我一直安慰自己医吊,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開白布逮京。 她就那樣靜靜地躺著卿堂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪懒棉。 梳的紋絲不亂的頭發(fā)上草描,一...
    開封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天览绿,我揣著相機(jī)與錄音,去河邊找鬼陶珠。 笑死挟裂,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的揍诽。 我是一名探鬼主播诀蓉,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼暑脆!你這毒婦竟也來了渠啤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤添吗,失蹤者是張志新(化名)和其女友劉穎沥曹,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體碟联,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡妓美,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鲤孵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片壶栋。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖普监,靈堂內(nèi)的尸體忽然破棺而出贵试,到底是詐尸還是另有隱情,我是刑警寧澤凯正,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布毙玻,位于F島的核電站,受9級(jí)特大地震影響廊散,放射性物質(zhì)發(fā)生泄漏桑滩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一允睹、第九天 我趴在偏房一處隱蔽的房頂上張望施符。 院中可真熱鬧,春花似錦擂找、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至慢洋,卻和暖如春塘雳,著一層夾襖步出監(jiān)牢的瞬間陆盘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來泰國打工败明, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留隘马,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓妻顶,卻偏偏與公主長(zhǎng)得像酸员,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子讳嘱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容