iOS管理多個彈窗彈出:FGPopupScheduler

GitHub 地址:FGPopupScheduler
支持 cocopods,使用簡便,效率不錯的基礎(chǔ)組件。

前言

前些天測試反饋當(dāng)新用戶剛打開APP的時候,由于彈窗過多,再加上還有半透明的引導(dǎo)層,經(jīng)常會出現(xiàn)彈窗互相覆蓋,甚至阻斷正常流程的情況。而需要解決這類問題,不單單要理清楚彈窗之間的依賴關(guān)系,還需要處理彈窗本身出現(xiàn)的條件。并且在每次有新的彈窗加入時都需要查看之前彈窗的邏輯。每一步都要耗費(fèi)開發(fā)資源。

所以我們的目的就是為了解決,如何拆分各個彈窗間的依賴關(guān)系,并在恰當(dāng)?shù)貢r刻依次顯示彈窗,解放何時顯示/何時隱藏的膠水代碼。

需求分析

首先是彈窗本身的需求

  • 彈窗顯示
  • 彈窗隱藏
  • 彈窗顯示需要滿足的條件

然后是關(guān)于彈窗與彈窗

  • 彈窗的優(yōu)先級
  • 彈窗是否會受到已顯示彈窗的影響

彈窗顯示有一個特征,就是同一個時刻只會顯示一個彈窗,并且可以是一個接一個顯示。如果采用采用隊(duì)列來管理的話,理所當(dāng)然地就需要額外處理插入、刪除、清空、遍歷等行為。

這一套流程下來貌似就解決了,但實(shí)際上當(dāng)把所有彈窗的統(tǒng)一交給一個調(diào)度器來管理的話,我們必須要考慮在什么時機(jī)顯示/隱藏這些彈窗才是更加合理的。

當(dāng)然,FGPopupScheduler 就能幫忙處理上面這些瑣碎的事情,而且不止于此。

實(shí)現(xiàn)分析

考慮到彈窗本身的多樣性,首先還是通過協(xié)議將隊(duì)列所需要的需求抽象處理放到<FGPopupView>中,。


@protocol FGPopupView <NSObject>

@optional
/*
 FGPopupSchedulerStrategyQueue會根據(jù) -showPopupView: 做顯示邏輯,如果含有動畫請實(shí)現(xiàn)-showPopupViewWithAnimation:方法
 */
- (void)showPopupView;

/*
 FGPopupSchedulerStrategyQueue會根據(jù) -dismissPopupView: 做隱藏邏輯,如果含有動畫請實(shí)現(xiàn)-showPopupViewWithAnimation:方法
 */
- (void)dismissPopupView;

/*
 FGPopupSchedulerStrategyQueue會根據(jù) -showPopupViewWithAnimation: 來做顯示邏輯。如果block不傳可能會出現(xiàn)意料外的問題
 */
- (void)showPopupViewWithAnimation:(FGPopupViewAnimationBlock)block;

/*
 FGPopupSchedulerStrategyQueue會根據(jù) -dismissPopupView: 做隱藏邏輯,如果含有動畫請實(shí)現(xiàn)-dismissPopupViewWithAnimation:方法,如果block不傳可能會出現(xiàn)意料外的問題
 */
- (void)dismissPopupViewWithAnimation:(FGPopupViewAnimationBlock)block;

/**
 FGPopupSchedulerStrategyQueue會根據(jù)-canRegisterFirstPopupView判斷,當(dāng)隊(duì)列順序輪到它的時候是否能夠成為響應(yīng)的第一個優(yōu)先級PopupView。默認(rèn)為YES
 */
- (BOOL)canRegisterFirstPopupViewResponder;



/** 0.4.0 新增*/

/**
 FGPopupSchedulerStrategyQueue 會根據(jù) - popupViewUntriggeredBehavior:來決定觸發(fā)時彈窗的顯示行為,默認(rèn)為 FGPopupViewUntriggeredBehaviorAwait
 */
- (FGPopupViewUntriggeredBehavior)popupViewUntriggeredBehavior;


/**
 FGPopupViewSwitchBehavior 會根據(jù) - popupViewSwitchBehavior:來決定已經(jīng)顯示的彈窗,是否會被后續(xù)更高優(yōu)先級的彈窗鎖影響,默認(rèn)為 FGPopupViewSwitchBehaviorAwait  ???? 只在FGPopupSchedulerStrategyPriority生效
 */
- (FGPopupViewSwitchBehavior)popupViewSwitchBehavior;

@end


關(guān)于彈窗顯示的順序和優(yōu)先級,實(shí)際操作中還會涉及到中途插入或者移除的操作,數(shù)據(jù)結(jié)構(gòu)更類似于鏈表,所以這里采用了C++的STL標(biāo)準(zhǔn)庫:list。

具體的策略如下

typedef NS_ENUM(NSUInteger, FGPopupSchedulerStrategy) {
    FGPopupSchedulerStrategyFIFO = 1 << 0,           //先進(jìn)先出
    FGPopupSchedulerStrategyLIFO = 1 << 1,           //后進(jìn)先出
    FGPopupSchedulerStrategyPriority = 1 << 2        //優(yōu)先級調(diào)度
};

實(shí)際上使用者還可以結(jié)合 FGPopupSchedulerStrategyPriority | FGPopupSchedulerStrategyFIFO 一起使用,來處理當(dāng)選擇優(yōu)先級策略時,如何決定同一優(yōu)先級彈窗的排序。

另外0.4.0新增了 FGPopupViewUntriggeredBehavior 和 FGPopupViewSwitchBehavior

FGPopupViewUntriggeredBehavior用于選擇當(dāng)響應(yīng)鏈中輪到該彈窗觸發(fā)的時候,如果不滿足顯示條件,是否會被直接丟棄。

typedef NS_ENUM(NSUInteger, FGPopupViewUntriggeredBehavior) {
    FGPopupViewUntriggeredBehaviorDiscard,          //當(dāng)彈窗觸發(fā)顯示邏輯,但未滿足條件時會被直接丟棄
    FGPopupViewUntriggeredBehaviorAwait,          //當(dāng)彈窗觸發(fā)顯示邏輯,但未滿足條件時會繼續(xù)等待
};

FGPopupViewSwitchBehavior 用于處理當(dāng)該彈窗已經(jīng)顯示的時候,是否會被更高優(yōu)先級的彈窗鎖替換。

typedef NS_ENUM(NSUInteger, FGPopupViewSwitchBehavior) {
    FGPopupViewSwitchBehaviorDiscard,  //當(dāng)該彈窗已經(jīng)顯示,如果后面來了彈窗優(yōu)先級更高的彈窗時,顯示更高優(yōu)先級彈窗并且當(dāng)前彈窗會被拋棄
    FGPopupViewSwitchBehaviorLatent,   //當(dāng)該彈窗已經(jīng)顯示,如果后面來了彈窗優(yōu)先級更高的彈窗時,顯示更高優(yōu)先級彈窗并且當(dāng)前彈窗重新進(jìn)入隊(duì)列, PS:優(yōu)先級相同時同 FGPopupViewSwitchBehaviorDiscard
    FGPopupViewSwitchBehaviorAwait,    //當(dāng)該彈窗已經(jīng)顯示時,不會被后續(xù)高優(yōu)線級的彈窗影響
};

通過hitTest來解決彈窗顯示條件的需求,如果根據(jù)當(dāng)前的命中的彈窗沒有通過hitTest,則會根據(jù)選擇的調(diào)度器策略,在當(dāng)前的list中獲取下一個彈窗進(jìn)行測試。

- (PopupElement *)_hitTestFirstPopupResponder{
    PopupElement *element;
    for(auto itor=_list.begin(); itor!=_list.end();) {
        PopupElement *temp = *itor;
        id<FGPopupView> data = temp.data;
        __block BOOL canRegisterFirstPopupViewResponder = YES;
        if ([data respondsToSelector:@selector(canRegisterFirstPopupViewResponder)]) {
            canRegisterFirstPopupViewResponder = [data canRegisterFirstPopupViewResponder];
        }
        
        if (canRegisterFirstPopupViewResponder) {
            element = temp;
            break;
        }
        else if([data respondsToSelector:@selector(popupViewUntriggeredBehavior)] && [data popupViewUntriggeredBehavior] == FGPopupViewUntriggeredBehaviorDiscard){
            itor = _list.erase(itor++);
        }
        else{
            itor++;
        }
    }
    return element;
}

由于通過FGPopupScheduler來統(tǒng)一管理所以的彈窗,所以彈窗上面時候觸發(fā)就需要組件自己來處理。這個筆者一共考慮了3個觸發(fā)情況

  • 添加彈窗對象的時候
  • 通過Runloop監(jiān)聽主線程空閑的時刻
  • 用戶主動觸發(fā)
    通過上面3種情況,差不多已經(jīng)能覆蓋所有的使用場景。

另外,還給調(diào)度器添加了suspended狀態(tài),來主動掛起/恢復(fù)彈窗隊(duì)列,用來控制當(dāng)前調(diào)度器是否能觸發(fā)hitTest進(jìn)而展示的邏輯。

此外組件支持線程安全。考慮到操作的時機(jī)可能在任意線程,組件通過pthread_mutex_t來保證線程安全。( pthread_mutex_t無法切換線程上鎖/解鎖已經(jīng)替換成信號量) 值得注意的是,彈窗的顯示過程會切換到主線程進(jìn)行,所以不需要去額外處理了。

至此,整個組件的業(yè)務(wù)是比較清晰了。FGPopupScheduler采用了狀態(tài)模式,
組件需要讓這三種處理方式可以自由的變動,所以采用策略模式來處理,下面是 UML 類圖:

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

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

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