iOS 狀態(tài)機(jī) 應(yīng)用 TransitionKit

iOS 狀態(tài)機(jī)
狀態(tài)機(jī) 的 概念 可以從網(wǎng)上搜索曲横。

此文章主要分析TransitionKit

TransitionKit 在iOS開發(fā)中的作用

  • 適用于流程化,狀態(tài)線性切換的場景不瓶;
  • 狀態(tài)切換有依賴和前提條件禾嫉;
  • 狀態(tài)切換只能由特定狀態(tài)切換到特定狀態(tài),不能隨意切換蚊丐,也不是可逆的熙参;
  • 可將特定狀態(tài)下的業(yè)務(wù)邏輯集中到一起管理

先來看看應(yīng)用,此處還是以直播應(yīng)用舉例子麦备。比如播放器的狀態(tài)孽椰,有 播放 暫停 加載中 加載錯誤這些。

一開始肯定是加載中凛篙,加載中的狀態(tài) 可以 切換到 加載錯誤 和 播放 播放 可以切換到 加載 錯誤 暫停黍匾。暫停可以切換到 播放呛梆。

每個狀態(tài)和下一個狀態(tài)的依賴是有順序的锐涯。每一個狀態(tài)要展示的樣子也有很大不同。接下來看我們的應(yīng)用填物。

- (void)setupStateMachine
{
    self.stateMachine = [[TKStateMachine alloc] init];
    
    __weak typeof(self) weakSelf = self;
    
    ///加載中 狀態(tài)
    TKState *loading = [TKState stateWithName:kLoading];
    [loading setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
        /// TODO
    }];
    ///播放狀態(tài)
    TKState *playing = [TKState stateWithName:kPlaying];
    [playing setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
         /// TODO
    }];
    
    [playing setDidExitStateBlock:^(TKState *state, TKTransition *transition) {
         /// TODO
    }];
    
    ///暫停狀態(tài)
    TKState *pause = [TKState stateWithName:kPause];
    [pause setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
         /// TODO
    }];
    ///播放完成狀態(tài)
    TKState *finish = [TKState stateWithName:kFinish];
    [finish setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
         /// TODO
    }];
    
    [self.stateMachine addStates:@[loading, playing, pause, finish]];
    [self.stateMachine setInitialState:finish];
    
    ///關(guān)聯(lián)事件
    TKEvent *loadingEvent = [TKEvent eventWithName:kLoading transitioningFromStates:@[playing, pause, finish] toState:loading];
    TKEvent *playingEvent = [TKEvent eventWithName:kPlaying transitioningFromStates:@[loading, pause, finish] toState:playing];
    TKEvent *pauseEvent = [TKEvent eventWithName:kPause transitioningFromStates:@[playing, loading] toState:pause];
    TKEvent *finishEvent = [TKEvent eventWithName:kFinish transitioningFromStates:@[loading, playing, pause] toState:finish];
    
    [_stateMachine addEvents:@[loadingEvent, playingEvent, pauseEvent, finishEvent]];
    
    [_stateMachine activate];
}

這里狀態(tài)是負(fù)責(zé)界面的變化什么的纹腌。比如加載中就在播放器上顯示一個菊花轉(zhuǎn)動霎终,比如暫停,按鈕狀態(tài)就要變化升薯。

接下來在狀態(tài)變化的時候觸發(fā)響應(yīng)的事件就好了莱褒。比如,從暫停狀態(tài)到播放狀態(tài)涎劈,這個時候觸發(fā)播放狀態(tài)的變化广凸。

[self.stateMachine fireEvent:kPlaying userInfo:nil error:nil];

回頭看我們定義播放事件的代碼

TKEvent *playingEvent = [TKEvent eventWithName:kPlaying transitioningFromStates:@[loading, pause, finish] toState:playing];

所以,只要前一個事件是 @[loading, pause, finish] 中的一個就能成功觸發(fā)蛛枚,否則炮障,事件不響應(yīng)!@ず颉!

我們看看源碼
主要由下面幾個類組成的

TKEvent                  ///事件對象
TKState                  ///狀態(tài)
TKStateMachine           ///狀態(tài)機(jī)管理中心
TKTransition             ///狀態(tài)切換的過程的信息

<code>TKStateMachine</code>負(fù)責(zé)管理狀態(tài)的企蹭,

初始化過程

- (id)init
{
    self = [super init];
    if (self) {
        self.mutableStates = [NSMutableSet set];
        self.mutableEvents = [NSMutableSet set];
        self.lock = [NSRecursiveLock new];
    }
    return self;
}

使用 <code> NSMutableSet </code>管理狀態(tài)和事件 lock 可以多線程調(diào)用

- (void)setInitialState:(TKState *)initialState
{
    TKRaiseIfActive();
    _initialState = initialState;
}

設(shè)置最開始的狀態(tài)白筹。<code> TKRaiseIfActive </code> 宏 用來判斷當(dāng)前的狀態(tài)是不是激活狀態(tài),如果是激活狀態(tài)谅摄,是不能修改的徒河,因?yàn)椋瑺顟B(tài)的動作有可能已經(jīng)開始執(zhí)行了~

重點(diǎn)看看 activate 和

- (BOOL)fireEvent:(id)eventOrEventName userInfo:(NSDictionary *)userInfo error:(NSError *__autoreleasing *)error

函數(shù)的實(shí)現(xiàn)過程

- (void)activate
{
///如果已經(jīng)是激活狀態(tài)送漠,激活肯定是無效的顽照。
    if (self.isActive) [NSException raise:NSInternalInconsistencyException format:@"The state machine has already been activated."];
    [self.lock lock];
    ///標(biāo)記已經(jīng)激活
    self.active = YES;
    ///調(diào)用對應(yīng)的blocks
    ///將要激活
    if (self.initialState.willEnterStateBlock) self.initialState.willEnterStateBlock(self.initialState, nil);
    ///設(shè)置當(dāng)前狀態(tài)
    self.currentState = self.initialState;
    ///已經(jīng)激活
    if (self.initialState.didEnterStateBlock) self.initialState.didEnterStateBlock(self.initialState, nil);
    [self.lock unlock];
}

這里有點(diǎn)類似 KVO 時候做的事情,在屬性改變之前和改變之后闽寡,通知一下關(guān)心屬性的對象代兵。

接下來看看觸發(fā)某事件的過程

- (BOOL)fireEvent:(id)eventOrEventName userInfo:(NSDictionary *)userInfo error:(NSError *__autoreleasing *)error
{
    [self.lock lock];
    ///設(shè)置激活狀態(tài)
    if (! self.isActive) [self activate];
    ///傳入的 eventOrEventName 如果是字符串,就通過字符串轉(zhuǎn)化成 TKEvent
    if (! [eventOrEventName isKindOfClass:[TKEvent class]] && ![eventOrEventName isKindOfClass:[NSString class]]) [NSException raise:NSInvalidArgumentException format:@"Expected a `TKEvent` object or `NSString` object specifying the name of an event, instead got a `%@` (%@)", [eventOrEventName class], eventOrEventName];
    TKEvent *event = [eventOrEventName isKindOfClass:[TKEvent class]] ? eventOrEventName : [self eventNamed:eventOrEventName];
    if (! event) [NSException raise:NSInvalidArgumentException format:@"Cannot find an Event named '%@'", eventOrEventName];

///檢查事件激活的條件是不是滿足爷狈!
    if (event.sourceStates != nil && ![event.sourceStates containsObject:self.currentState]) {
        NSString *failureReason = [NSString stringWithFormat:@"An attempt was made to fire the '%@' event while in the '%@' state, but the event can only be fired from the following states: %@", event.name, self.currentState.name, [[event.sourceStates valueForKey:@"name"] componentsJoinedByString:@", "]];
        NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"The event cannot be fired from the current state.", NSLocalizedFailureReasonErrorKey: failureReason };
        if (error) *error = [NSError errorWithDomain:TKErrorDomain code:TKInvalidTransitionError userInfo:userInfo];
        [self.lock unlock];
        return NO;
    }

    TKTransition *transition = [TKTransition transitionForEvent:event fromState:self.currentState inStateMachine:self userInfo:userInfo];
    ///詢問外部接口植影,這個事件能不能觸發(fā)。
    if (event.shouldFireEventBlock) {
        if (! event.shouldFireEventBlock(event, transition)) {
            NSString *failureReason = [NSString stringWithFormat:@"An attempt to fire the '%@' event was declined because `shouldFireEventBlock` returned `NO`.", event.name];
            NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"The event declined to be fired.", NSLocalizedFailureReasonErrorKey: failureReason };
            if (error) *error = [NSError errorWithDomain:TKErrorDomain code:TKTransitionDeclinedError userInfo:userInfo];
            [self.lock unlock];
            return NO;
        }
    }
    /// 開始切換狀態(tài)
    TKState *oldState = self.currentState;
    TKState *newState = event.destinationState;
    /// 切換狀態(tài)中的事件通知涎永。
    if (event.willFireEventBlock) event.willFireEventBlock(event, transition);
    
    if (oldState.willExitStateBlock) oldState.willExitStateBlock(oldState, transition);
    if (newState.willEnterStateBlock) newState.willEnterStateBlock(newState, transition);
    self.currentState = newState;
    
    NSMutableDictionary *notificationInfo = [userInfo mutableCopy] ?: [NSMutableDictionary dictionary];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
    [notificationInfo addEntriesFromDictionary:@{ TKStateMachineDidChangeStateOldStateUserInfoKey: oldState,
                                                  TKStateMachineDidChangeStateNewStateUserInfoKey: newState,
                                                  TKStateMachineDidChangeStateEventUserInfoKey: event,
#pragma clang diagnostic pop
                                                  TKStateMachineDidChangeStateTransitionUserInfoKey: transition }];
    [[NSNotificationCenter defaultCenter] postNotificationName:TKStateMachineDidChangeStateNotification object:self userInfo:notificationInfo];
    
    if (oldState.didExitStateBlock) oldState.didExitStateBlock(oldState, transition);
    if (newState.didEnterStateBlock) newState.didEnterStateBlock(newState, transition);
    
    if (event.didFireEventBlock) event.didFireEventBlock(event, transition);
    [self.lock unlock];
    
    return YES;
}
if (event.sourceStates != nil && ![event.sourceStates containsObject:self.currentState])

如果當(dāng)前要激活的事件存在sourceStates 并且 sourceStates 不包含 currentState 那么事件肯定不能觸發(fā)了思币。因?yàn)檫`背了之前的講的條件,狀態(tài)的變化是線性化的羡微,并且有前提條件谷饿,并且不能隨意切換!

TKTransition 狀態(tài)變化過程中的信息妈倔。

- (TKEvent *)eventNamed:(NSString *)name
{
    for (TKEvent *event in self.mutableEvents) {
        if ([event.name isEqualToString:name]) return event;
    }
    return nil;
}

eventNamed 函數(shù) 通過事件的名稱查找 事件博投。前提條件是事件已經(jīng)加入到了事件管理的 set 中。启涯。 方便容錯贬堵,其他地方如果使用到 TKEvent 的時候恃轩,直接定義參數(shù)類型為 id 類型,使用的時候再通過類型推導(dǎo)黎做! 因?yàn)橥獠縿?chuàng)建事件的時候叉跛,不一定要保存一份,比如我們上面創(chuàng)建的過程蒸殿!

其他的代碼筷厘,都比較好理解了。

總之宏所,碰到跟狀態(tài)相關(guān)的需求酥艳,可以考慮 TKTransition 這個第三庫!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末爬骤,一起剝皮案震驚了整個濱河市充石,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌霞玄,老刑警劉巖骤铃,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異坷剧,居然都是意外死亡惰爬,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進(jìn)店門惫企,熙熙樓的掌柜王于貴愁眉苦臉地迎上來撕瞧,“玉大人,你說我怎么就攤上這事狞尔〈园妫” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵沪么,是天一觀的道長硼婿。 經(jīng)常有香客問我,道長禽车,這世上最難降的妖魔是什么寇漫? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮殉摔,結(jié)果婚禮上州胳,老公的妹妹穿的比我還像新娘。我一直安慰自己逸月,他們只是感情好栓撞,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般瓤湘。 火紅的嫁衣襯著肌膚如雪瓢颅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天弛说,我揣著相機(jī)與錄音挽懦,去河邊找鬼。 笑死木人,一個胖子當(dāng)著我的面吹牛信柿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播醒第,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼渔嚷,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了稠曼?” 一聲冷哼從身側(cè)響起形病,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎霞幅,沒想到半個月后窒朋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蝗岖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了榔至。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抵赢。...
    茶點(diǎn)故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖唧取,靈堂內(nèi)的尸體忽然破棺而出铅鲤,到底是詐尸還是另有隱情,我是刑警寧澤枫弟,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布邢享,位于F島的核電站,受9級特大地震影響淡诗,放射性物質(zhì)發(fā)生泄漏骇塘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一韩容、第九天 我趴在偏房一處隱蔽的房頂上張望款违。 院中可真熱鬧,春花似錦群凶、人聲如沸插爹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赠尾。三九已至力穗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間气嫁,已是汗流浹背当窗。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留杉编,地道東北人超全。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像邓馒,于是被迫代替她去往敵國和親嘶朱。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評論 2 359

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