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 類圖: