ReactiveCocoa2實戰

以前已經寫過兩篇關於ReactiveCocoa(如下簡稱RAC)的文章了,但主要也是在闡述基本的概念和使用,這篇文章將會從實戰的角度來看看RAC到底解決了哪些問題,帶來了哪些方便,以及遇到的一些坑。html

概述

爲何要使用RAC?

一個怪怪的東西,從Demo看也沒有讓代碼變得更好、更短,相反還形成理解上的困難,真的有必要去學它麼?相信這是大多數人在接觸RAC時的想法。RAC不是單一功能的模塊,它是一個Framework,提供了一整套解決方案。其核心思想是「響應數據的變化」,在這個基礎上有了Signal的概念,進而能夠幫助減小狀態變量(能夠參考jspahrsummers的PPT),使用MVVM架構,統一的異步編程模型等等。前端

爲何RAC更加適合編寫Cocoa App?說這個以前,咱們先來看下Web前端編程,由於有些類似之處。目前很火的AngularJS有一個很重要的特性:數據與視圖綁定。就是當數據變化時,視圖不須要額外的處理,即可正確地呈現最新的數據。而這也是RAC的亮點之一。RAC與Cocoa的編程模式,有點像AngularJS和jQuery。因此要了解RAC,須要先在觀念上作調整。react

如下面這個Cell爲例git

正常的寫法多是這樣,很直觀。github

- (void)configureWithItem:(HBItem *)item
{
    self.username.text = item.text;
    [self.avatarImageView setImageWithURL: item.avatarURL];
    // 其餘的一些設置
}

但若是用RAC,可能就是這樣objective-c

- (id)init
{
    if (self = [super init]) {
        @weakify(self);
        [RACObserve(self, viewModel) subscribeNext:^(HBItemViewModel *viewModel) {
            @strongify(self);
            self.username.text = viewModel.item.text;
            [self.avatarImageView setImageWithURL: viewModel.item.avatarURL];
            // 其餘的一些設置
     }];
    }
}

也就是先把數據綁定,接下來只要數據有變化,就會自動響應變化。在這裏,每次viewModel改變時,內容就會自動變成該viewModel的內容。編程

Signal

Signal是RAC的核心,爲了幫助理解,畫了這張簡化圖緩存

這裏的數據源和sendXXX,能夠理解爲函數的參數和返回值。當Signal處理完數據後,能夠向下一個Signal或Subscriber傳送數據。能夠看到上半部分的兩個Signal是冷的(cold),至關於實現了某個函數,但該函數沒有被調用。同時也說明了Signal能夠被組合使用,好比RACSignal *signalB = [signalA map:^id(id x){return x}],或RACSignal *signalB = [signalA take:1]等等。網絡

當signal被subscribe時,就會處於熱(hot)的狀態,也就是該函數會被執行。好比上面的第二張圖,首先signalA可能發了一個網絡請求,拿到結果後,把數據經過sendNext方法傳遞到下一個signal,signalB能夠根據須要作進一步處理,好比轉換成相應的Model,轉換完後再sendNext到subscriber,subscriber拿到數據後,再改變ViewModel,同時由於View已經綁定了ViewModel,因此拿到的數據會自動在View裏呈現。架構

還有,一個signal能夠被多個subscriber訂閱,這裏怕顯得太亂就沒有畫出來,但每次被新的subscriber訂閱時,都會致使數據源的處理邏輯被觸發一次,這頗有可能致使意想不到的結果,須要注意一下。

當數據從signal傳送到subscriber時,還能夠經過doXXX來作點事情,好比打印數據。

經過這張圖能夠看到,這很是像中學時學的函數,好比 f(x) = y,某一個函數的輸出又能夠做爲另外一個函數的輸入,好比 f(f(x)) = z,這也正是「函數響應式編程」(FRP)的核心。

有些地方須要注意下,好比把signal做爲local變量時,若是沒有被subscribe,那麼方法執行完後,該變量會被dealloc。但若是signal有被subscribe,那麼subscriber會持有該signal,直到signal sendCompleted或sendError時,纔會解除持有關係,signal纔會被dealloc。

RACCommand

RACCommand是RAC很重要的組成部分,能夠節省不少時間而且讓你的App變得更Robust,這篇文章能夠幫助你更深刻的理解,這裏簡單作一下介紹。

RACCommand 一般用來表示某個Action的執行,好比點擊Button。它有幾個比較重要的屬性:executionSignals / errors / executing。

  • executionSignals是signal of signals,若是直接subscribe的話會獲得一個signal,而不是咱們想要的value,因此通常會配合switchToLatest
  • errors。跟正常的signal不同,RACCommand的錯誤不是經過sendError來實現的,而是經過errors屬性傳遞出來的。
  • executing表示該command當前是否正在執行。

假設有這麼個需求:當圖片載入完後,分享按鈕纔可用。那麼能夠這樣:

RACSignal *imageAvailableSignal = [RACObserve(self, imageView.image) map:id^(id x){return x ? @YES : @NO}];
self.shareButton.rac_command = [[RACCommand alloc] initWithEnabled:imageAvailableSignal signalBlock:^RACSignal *(id input) {
    // do share logic
}];

除了與UIControl綁定以外,也能夠手動執行某個command,好比雙擊圖片點贊,就能夠這麼實現。

// ViewModel.m
- (instancetype)init
{
    self = [super init];
    if (self) {
        void (^updatePinLikeStatus)() = ^{
            self.pin.likedCount = self.pin.hasLiked ? self.pin.likedCount - 1 : self.pin.likedCount + 1;
            self.pin.hasLiked = !self.pin.hasLiked;
        };
        
        _likeCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
            // 先展現效果,再發送請求
            updatePinLikeStatus();
            return [[HBAPIManager sharedManager] likePinWithPinID:self.pin.pinID];
        }];
        
        [_likeCommand.errors subscribeNext:^(id x) {
            // 發生錯誤時,回滾
            updatePinLikeStatus();
        }];
    }
    return self;
}

// ViewController.m
- (void)viewDidLoad
{
    [super viewDidLoad];
    // ...
    @weakify(self);
    [RACObserve(self, viewModel.hasLiked) subscribeNext:^(id x){
        @strongify(self);
        self.pinLikedCountLabel.text = self.viewModel.likedCount;
        self.likePinImageView.image = [UIImage imageNamed:self.viewModel.hasLiked ? @"pin_liked" : @"pin_like"];
    }];
    
    UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] init];
    tapGesture.numberOfTapsRequired = 2;
    [[tapGesture rac_gestureSignal] subscribeNext:^(id x) {
        [self.viewModel.likeCommand execute:nil];
    }];
}

再好比某個App要經過Twitter登陸,同時容許取消登陸,就能夠這麼作 (source)

_twitterLoginCommand = [[RACCommand alloc] initWithSignalBlock:^(id _) {
      @strongify(self);
      return [[self 
          twitterSignInSignal] 
          takeUntil:self.cancelCommand.executionSignals];
    }];

RAC(self.authenticatedUser) = [self.twitterLoginCommand.executionSignals switchToLatest];

經常使用的模式

map + switchToLatest

switchToLatest: 的做用是自動切換signal of signals到最後一個,好比以前的command.executionSignals就可使用switchToLatest:

map:的做用很簡單,對sendNext的value作一下處理,返回一個新的值。

若是把這兩個結合起來就有意思了,想象這麼個場景,當用戶在搜索框輸入文字時,須要經過網絡請求返回相應的hints,每當文字有變更時,須要取消上一次的請求,就可使用這個配搭。這裏用另外一個Demo,簡單演示一下

NSArray *pins = @[@172230988, @172230947, @172230899, @172230777, @172230707];
__block NSInteger index = 0;

RACSignal *signal = [[[[RACSignal interval:0.1 onScheduler:[RACScheduler scheduler]]
                        take:pins.count]
                        map:^id(id value) {
                            return [[[HBAPIManager sharedManager] fetchPinWithPinID:[pins[index++] intValue]] doNext:^(id x) {
                                NSLog(@"這裏只會執行一次");
                            }];
                        }]
                        switchToLatest];

[signal subscribeNext:^(HBPin *pin) {
    NSLog(@"pinID:%d", pin.pinID);
} completed:^{
    NSLog(@"completed");
}];

// output
// 2014-06-05 17:40:49.851 這裏只會執行一次
// 2014-06-05 17:40:49.851 pinID:172230707
// 2014-06-05 17:40:49.851 completed

takeUntil

takeUntil:someSignal 的做用是當someSignal sendNext時,當前的signal就sendCompleted,someSignal就像一個拳擊裁判,哨聲響起就意味着比賽終止。

它的經常使用場景之一是處理cell的button的點擊事件,好比點擊Cell的詳情按鈕,須要push一個VC,就能夠這樣:

[[[cell.detailButton
    rac_signalForControlEvents:UIControlEventTouchUpInside]
    takeUntil:cell.rac_prepareForReuseSignal]
    subscribeNext:^(id x) {
        // generate and push ViewController
}];

若是不加takeUntil:cell.rac_prepareForReuseSignal,那麼每次Cell被重用時,該button都會被addTarget:selector

替換Delegate

出現這種需求,一般是由於須要對Delegate的多個方法作統一的處理,這時就能夠造一個signal出來,每次該Delegate的某些方法被觸發時,該signal就會sendNext

@implementation UISearchDisplayController (RAC)
- (RACSignal *)rac_isActiveSignal {
    self.delegate = self;
    RACSignal *signal = objc_getAssociatedObject(self, _cmd);
    if (signal != nil) return signal;
    
    
    RACSignal *didBeginEditing = [[self rac_signalForSelector:@selector(searchDisplayControllerDidBeginSearch:) 
                                        fromProtocol:@protocol(UISearchDisplayDelegate)] mapReplace:@YES];
    RACSignal *didEndEditing = [[self rac_signalForSelector:@selector(searchDisplayControllerDidEndSearch:) 
                                      fromProtocol:@protocol(UISearchDisplayDelegate)] mapReplace:@NO];
    signal = [RACSignal merge:@[didBeginEditing, didEndEditing]];
    
    
    objc_setAssociatedObject(self, _cmd, signal, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return signal;
}
@end

代碼源於此文

使用ReactiveViewModel的didBecomActiveSignal

ReactiveViewModel是另外一個project, 後面的MVVM中會講到,一般的作法是在VC裏設置VM的active屬性(RVMViewModel自帶該屬性),而後在VM裏subscribeNext didBecomActiveSignal,好比當Active時,獲取TableView的最新數據。

RACSubject的使用場景

通常不推薦使用RACSubject,由於它過於靈活,濫用的話容易致使複雜度的增長。但有一些場景用一下仍是比較方便的,好比ViewModel的errors。

ViewModel通常會有多個RACCommand,那這些commands若是出現error了該如何處理呢?比較方便的方法以下:

// HBCViewModel.h

#import "RVMViewModel.h"

@class RACSubject;

@interface HBCViewModel : RVMViewModel
@property (nonatomic) RACSubject *errors;
@end



// HBCViewModel.m

#import "HBCViewModel.h"
#import 

@implementation HBCViewModel

- (instancetype)init
{
    self = [super init];
    if (self) {
        _errors = [RACSubject subject];
    }
    return self;
}

- (void)dealloc
{
    [_errors sendCompleted];
}
@end

// Some Other ViewModel inherit HBCViewModel

- (instancetype)init
{
    _fetchLatestCommand = [RACCommand alloc] initWithSignalBlock:^RACSignal *(id input){
        // fetch latest data
 }];

    _fetchMoreCommand = [RACCommand alloc] initWithSignalBlock:^RACSignal *(id input){
        // fetch more data
 }];

    [self.didBecomeActiveSignal subscribeNext:^(id x) {
        [_fetchLatestCommand execute:nil];
    }];
    
    [[RACSignal
        merge:@[
                _fetchMoreCommand.errors,
                _fetchLatestCommand.errors
                ]] subscribe:self.errors];

}

rac_signalForSelector

rac_signalForSelector: 這個方法會返回一個signal,當selector執行完時,會sendNext,也就是當某個方法調用完後再額外作一些事情。用在category會比較方便,由於Category重寫父類的方法時,不能再經過[super XXX]來調用父類的方法,固然也能夠手寫Swizzle來實現,不過有了rac_signalForSelector:就方便多了。

rac_signalForSelector: fromProtocol: 能夠直接實現對protocol的某個方法的實現(聽着有點彆扭呢),好比,咱們想實現UIScrollViewDelegate的某些方法,能夠這麼寫

[[self rac_signalForSelector:@selector(scrollViewDidEndDecelerating:) fromProtocol:@protocol(UIScrollViewDelegate)] subscribeNext:^(RACTuple *tuple) {
    // do something
}];

[[self rac_signalForSelector:@selector(scrollViewDidScroll:) fromProtocol:@protocol(UIScrollViewDelegate)] subscribeNext:^(RACTuple *tuple) {
    // do something
}];

self.scrollView.delegate = nil;
self.scrollView.delegate = self;

注意,這裏的delegate須要先設置爲nil,再設置爲self,而不能直接設置爲self,若是self已是該scrollView的Delegate的話。

有時,咱們想對selector的返回值作一些處理,但很遺憾RAC不支持,若是真的有須要的話,可使用Aspects

MVVM

這是一個大話題,若是有耐心,且英文還不錯的話,能夠看一下Cocoa Samurai的這兩篇文章。PS: Facebook Paper就是基於MVVM構建的。

MVVM是Model-View-ViewModel的簡稱,它們之間的關係以下

能夠看到View(實際上是ViewController)持有ViewModel,這樣作的好處是ViewModel更加獨立且可測試,ViewModel裏不該包含任何View相關的元素,哪怕換了一個View也能正常工做。並且這樣也能讓View/ViewController「瘦」下來。

ViewModel主要作的事情是做爲View的數據源,因此一般會包含網絡請求。

或許你會疑惑,ViewController哪去了?在MVVM的世界裏,ViewController已經成爲了View的一部分。它的主要職責是將VM與View綁定、響應VM數據的變化、調用VM的某個方法、與其餘的VC打交道。

而RAC爲MVVM帶來很大的便利,好比RACCommand, UIKit的RAC Extension等等。使用MVVM不必定能減小代碼量,但能下降代碼的複雜度。

如下面這個需求爲例,要求大圖滑動結束時,底部的縮略圖滾動到對應的位置,並高亮該縮略圖;同時底部的縮略圖被選中時,大圖也要變成該縮略圖的大圖。

個人思路是橫向滾動的大圖是一個collectionView,該collectionView是當前頁面VC的一個property。底部能夠滑動的縮略圖是一個childVC的collectionView,這兩個collectionView共用一套VM,而且各自RACObserve感興趣的property。

好比大圖滑到下一頁時,會改變VM的indexPath屬性,而底部的collectionView所在的VC正好對該indexPath感興趣,只要indexPath變化就滾動到相應的Item

// childVC

- (void)viewDidLoad
{
    [super viewDidLoad];

    @weakify(self);
    [RACObserve(self, viewModel.indexPath) subscribeNext:^(NSNumber *index) {
        @strongify(self);
        [self scrollToIndexPath];
    }];
}

- (void)scrollToIndexPath
{
    if (self.collectionView.subviews.count) {
        NSIndexPath *indexPath = self.viewModel.indexPath;
        [self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
        [self.collectionView.subviews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL *stop) {
            view.layer.borderWidth = 0;
        }];
        UIView *view = [self.collectionView cellForItemAtIndexPath:indexPath];
        view.layer.borderWidth = kHBPinsNaviThumbnailPadding;
        view.layer.borderColor = [UIColor whiteColor].CGColor;
    }
}

當點擊底部的縮略圖時,上面的大圖也要作出變化,也一樣能夠經過RACObserve indexPath來實現

// PinsViewController.m
- (void)viewDidLoad
{
    [super viewDidLoad];
    @weakify(self);
    [[RACObserve(self, viewModel.indexPath)
        skip:1]
        subscribeNext:^(NSIndexPath *indexPath) {
            @strongify(self);
            [self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
    }];
}

這裏有一個小技巧,當Cell裏的元素比較複雜時,咱們能夠給Cell也準備一個ViewModel,這個CellViewModel能夠由上一層的ViewModel提供,這樣Cell若是須要相應的數據,直接跟CellViewModel要便可,CellViewModel也能夠包含一些command,好比likeCommand。假如點擊Cell時,要作一些處理,也很方便。

// CellViewModel已經在ViewModel裏準備好了
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    HBPinsCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
    cell.viewModel = self.viewModel.cellViewModels[indexPath.row];
    return cell;
}

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    HBCellViewModel *cellViewModel = self.viewModel.cellViewModels[indexPath.row];
    // 對cellViewModel執行某些操做,由於Cell已經與cellViewModel綁定,因此cellViewModel的改變也會反映到Cell上
 // 或拿到cellViewModel的數據來執行某些操做
}

ViewModel中signal, property, command的使用

初次使用RAC+MVVM時,每每會疑惑,何時用signal,何時用property,何時用command?

通常來講可使用property的就直接使用,不必再轉換成signal,外部RACObserve便可。使用signal的場景通常是涉及到多個property或多個signal合併爲一個signal。command每每與UIControl/網絡請求掛鉤。

常見場景的處理

檢查本地緩存,若是失效則去請求網絡數據並緩存到本地

來源

- (RACSignal *)loadData {
    return [[RACSignal 
        createSignal:^(id<</span>RACSubscriber> subscriber) {
            // If the cache is valid then we can just immediately send the 
            // cached data and be done.
            if (self.cacheValid) {
                [subscriber sendNext:self.cachedData];
                [subscriber sendCompleted];
            } else {
                [subscriber sendError:self.staleCacheError];
            }
        }] 
        // Do the subscription work on some random scheduler, off the main 
        // thread.
        subscribeOn:[RACScheduler scheduler]];
}

- (void)update {
    [[[[self 
        loadData]
        // Catch the error from -loadData. It means our cache is stale. Update
        // our cache and save it.
        catch:^(NSError *error) {
            return [[self updateCachedData] doNext:^(id data) {
                [self cacheData:data];
            }];
        }] 
        // Our work up until now has been on a background scheduler. Get our 
        // results delivered on the main thread so we can do UI work.
        deliverOn:RACScheduler.mainThreadScheduler]
        subscribeNext:^(id data) {
            // Update your UI based on `data`.

            // Update again after `updateInterval` seconds have passed.
            [[RACSignal interval:updateInterval] take:1] subscribeNext:^(id _) {
                [self update];
            }];
        }]; 
}

檢測用戶名是否可用

來源

- (void)setupUsernameAvailabilityChecking {
    RAC(self, availabilityStatus) = [[[RACObserve(self.userTemplate, username)
                                      throttle:kUsernameCheckThrottleInterval] //throttle表示interval時間內若是有sendNext,則放棄該nextValue
                                      map:^(NSString *username) {
                                          if (username.length == 0) return [RACSignal return:@(UsernameAvailabilityCheckStatusEmpty)];
                                          return [[[[[FIBAPIClient sharedInstance]
                                                getUsernameAvailabilityFor:username ignoreCache:NO]
                                              map:^(NSDictionary *result) {
                                                  NSNumber *existsNumber = result[@"exists"];
                                                  if (!existsNumber) return @(UsernameAvailabilityCheckStatusFailed);
                                                  UsernameAvailabilityCheckStatus status = [existsNumber boolValue] ? UsernameAvailabilityCheckStatusUnavailable : UsernameAvailabilityCheckStatusAvailable;
                                                  return @(status);
                                              }]
                                             catch:^(NSError *error) {
                                                  return [RACSignal return:@(UsernameAvailabilityCheckStatusFailed)];
                                              }] startWith:@(UsernameAvailabilityCheckStatusChecking)];
                                      }]
                                      switchToLatest];
}

能夠看到這裏也使用了map + switchToLatest模式,這樣就能夠自動取消上一次的網絡請求。

startWith的內部實現是concat,這裏表示先將狀態置爲checking,而後再根據網絡請求的結果設置狀態。

使用takeUntil:來處理Cell的button點擊

這個上面已經提到過了。

token過時後自動獲取新的

開發APIClient時,會用到AccessToken,這個Token過一段時間會過時,須要去請求新的Token。比較好的用戶體驗是當token過時後,自動去獲取新的Token,拿到後繼續上一次的請求,這樣對用戶是透明的。

RACSignal *requestSignal = [RACSignal createSignal:^RACDisposable *(id<</span>RACSubscriber> subscriber) {
        // suppose first time send request, access token is expired or invalid
        // and next time it is correct.
        // the block will be triggered twice.
        static BOOL isFirstTime = 0;
        NSString *url = @"http://httpbin.org/ip";
        if (!isFirstTime) {
            url = @"http://nonexists.com/error";
            isFirstTime = 1;
        }
        NSLog(@"url:%@", url);
        [[AFHTTPRequestOperationManager manager] GET:url parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
            [subscriber sendNext:responseObject];
            [subscriber sendCompleted];
        } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
            [subscriber sendError:error];
        }];
        return nil;
    }];
    
    self.statusLabel.text = @"sending request...";
    [[requestSignal catch:^RACSignal *(NSError *error) {
        self.statusLabel.text = @"oops, invalid access token";
        
        // simulate network request, and we fetch the right access token
        return [[RACSignal createSignal:^RACDisposable *(id<</span>RACSubscriber> subscriber) {
            double delayInSeconds = 1.0;
            dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
            dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
                [subscriber sendNext:@YES];
                [subscriber sendCompleted];
            });
            return nil;
        }] concat:requestSignal];
    }] subscribeNext:^(id x) {
        if ([x isKindOfClass:[NSDictionary class]]) {
            self.statusLabel.text = [NSString stringWithFormat:@"result:%@", x[@"origin"]];
        }
    } completed:^{
        NSLog(@"completed");
    }];

注意事項

RAC我本身感受遇到的幾個難點是: 1) 理解RAC的理念。 2) 熟悉經常使用的API。3) 針對某些特定的場景,想出比較合理的RAC處理方式。不過看多了,寫多了,想多了就會慢慢適應。下面是我在實踐過程當中遇到的一些小坑。

ReactiveCocoaLayout

有時Cell的內容涉及到動態的高度,就會想到用Autolayout來佈局,但RAC已經爲咱們準備好了ReactiveCocoaLayout,因此我想不妨就拿來用一下。

ReactiveCocoaLayout的使用比如「批地」和「蓋房」,先經過insetWidth:height:nullRect從某個View中劃出一小塊,拿到以後還能夠經過divideWithAmount:padding:fromEdge 再分紅兩塊,或sliceWithAmount:fromEdge再分出一塊。這些方法返回的都是signal,因此能夠經過RAC(self.view, frame) = someRectSignal 這樣來實現綁定。但在實踐中發現性能不是很好,多批了幾塊地就容易形成主線程卡頓。

因此ReactiveCocoaLayout最好不用或少用。

調試

剛開始寫RAC時,每每會遇到這種狀況,滿屏的調用棧信息都是RAC的,要找出真正出現問題的地方不容易。曾經有一次在使用[RACSignal combineLatest: reduce:^id{}]時,忘了在Block裏返回value,而Xcode也沒有提示warning,而後就是莫名其妙地掛起了,跳到了彙編上,也沒有調用棧信息,這時就只能經過最古老的註釋代碼的方式來找到問題的根源。

不過寫多了以後,通常不太會犯這種低級錯誤。

strongify / weakify dance

由於RAC不少操做都是在Block中完成的,這塊最多見的問題就是在block直接把self拿來用,形成block和self的retain cycle。因此須要經過@strongify@weakify來消除循環引用。

有些地方很容易被忽略,好比RACObserve(thing, keypath),看上去並無引用self,因此在subscribeNext時就忘記了weakify/strongify。但事實上RACObserve老是會引用self,即便target不是self,因此只要有RACObserve的地方都要使用weakify/strongify。

小結

以上是我在作花瓣客戶端和side project時總結的一些經驗,希望能帶來一些幫助,有誤的地方也歡迎指正和探討。

推薦一下jspahrsummers的這個project,雖然是用RAC3.0寫的,但不少理念也能夠用到RAC2上面。

最後感謝Github的iOS工程師們,感謝大家帶來了RAC,以及在Issues裏的耐心解答。

相關文章
相關標籤/搜索