GitHub 地址:FGPopupScheduler
支持 cocopods,使用简便,效率不错的基础组件。
前言
前些天测试反馈当新用户刚打开APP的时候,由于弹窗过多,再加上还有半透明的引导层,经常会出现弹窗互相覆盖,甚至阻断正常流程的情况。而需要解决这类问题,不单单要理清楚弹窗之间的依赖关系,还需要处理弹窗本身出现的条件。并且在每次有新的弹窗加入时都需要查看之前弹窗的逻辑。每一步都要耗费开发资源。
所以我们的目的就是为了解决,如何拆分各个弹窗间的依赖关系,并在恰当地时刻依次显示弹窗,解放何时显示/何时隐藏的胶水代码。
需求分析
首先是弹窗本身的需求
- 弹窗显示
- 弹窗隐藏
- 弹窗显示需要满足的条件
然后是关于弹窗与弹窗
- 弹窗的优先级
- 弹窗是否会受到已显示弹窗的影响
弹窗显示有一个特征,就是同一个时刻只会显示一个弹窗,并且可以是一个接一个显示。如果采用采用队列来管理的话,理所当然地就需要额外处理插入、删除、清空、遍历等行为。
这一套流程下来貌似就解决了,但实际上当把所有弹窗的统一交给一个调度器来管理的话,我们必须要考虑在什么时机显示/隐藏这些弹窗才是更加合理的。
当然,FGPopupScheduler 就能帮忙处理上面这些琐碎的事情,而且不止于此。
实现分析
考虑到弹窗本身的多样性,首先还是通过协议将队列所需要的需求抽象处理放到
中,。
@protocol FGPopupView
@optional
/*
FGPopupSchedulerStrategyQueue会根据 -showPopupView: 做显示逻辑,如果含有动画请实现-showPopupViewWithAnimation:方法
*/
- (void)showPopupView;
/*
FGPopupSchedulerStrategyQueue会根据 -dismissPopupView: 做隐藏逻辑,如果含有动画请实现-showPopupViewWithAnimation:方法
*/
- (void)dismissPopupView;
/*
FGPopupSchedulerStrategyQueue会根据 -showPopupViewWithAnimation: 来做显示逻辑。如果block不传可能会出现意料外的问题
*/
- (void)showPopupViewWithAnimation:(FGPopupViewAnimationBlock)block;
/*
FGPopupSchedulerStrategyQueue会根据 -dismissPopupView: 做隐藏逻辑,如果含有动画请实现-dismissPopupViewWithAnimation:方法,如果block不传可能会出现意料外的问题
*/
- (void)dismissPopupViewWithAnimation:(FGPopupViewAnimationBlock)block;
/**
FGPopupSchedulerStrategyQueue会根据-canRegisterFirstPopupView判断,当队列顺序轮到它的时候是否能够成为响应的第一个优先级PopupView。默认为YES
*/
- (BOOL)canRegisterFirstPopupViewResponder;
/** 0.4.0 新增*/
/**
FGPopupSchedulerStrategyQueue 会根据 - popupViewUntriggeredBehavior:来决定触发时弹窗的显示行为,默认为 FGPopupViewUntriggeredBehaviorAwait
*/
- (FGPopupViewUntriggeredBehavior)popupViewUntriggeredBehavior;
/**
FGPopupViewSwitchBehavior 会根据 - popupViewSwitchBehavior:来决定已经显示的弹窗,是否会被后续更高优先级的弹窗锁影响,默认为 FGPopupViewSwitchBehaviorAwait ⚠️⚠️ 只在FGPopupSchedulerStrategyPriority生效
*/
- (FGPopupViewSwitchBehavior)popupViewSwitchBehavior;
@end
关于弹窗显示的顺序和优先级,实际操作中还会涉及到中途插入或者移除的操作,数据结构更类似于链表,所以这里采用了C++的STL标准库:list。
具体的策略如下
typedef NS_ENUM(NSUInteger, FGPopupSchedulerStrategy) {
FGPopupSchedulerStrategyFIFO = 1 << 0, //先进先出
FGPopupSchedulerStrategyLIFO = 1 << 1, //后进先出
FGPopupSchedulerStrategyPriority = 1 << 2 //优先级调度
};
实际上使用者还可以结合 FGPopupSchedulerStrategyPriority | FGPopupSchedulerStrategyFIFO
一起使用,来处理当选择优先级策略时,如何决定同一优先级弹窗的排序。
另外0.4.0新增了 FGPopupViewUntriggeredBehavior 和 FGPopupViewSwitchBehavior
FGPopupViewUntriggeredBehavior用于选择当响应链中轮到该弹窗触发的时候,如果不满足显示条件,是否会被直接丢弃。
typedef NS_ENUM(NSUInteger, FGPopupViewUntriggeredBehavior) {
FGPopupViewUntriggeredBehaviorDiscard, //当弹窗触发显示逻辑,但未满足条件时会被直接丢弃
FGPopupViewUntriggeredBehaviorAwait, //当弹窗触发显示逻辑,但未满足条件时会继续等待
};
FGPopupViewSwitchBehavior 用于处理当该弹窗已经显示的时候,是否会被更高优先级的弹窗锁替换。
typedef NS_ENUM(NSUInteger, FGPopupViewSwitchBehavior) {
FGPopupViewSwitchBehaviorDiscard, //当该弹窗已经显示,如果后面来了弹窗优先级更高的弹窗时,显示更高优先级弹窗并且当前弹窗会被抛弃
FGPopupViewSwitchBehaviorLatent, //当该弹窗已经显示,如果后面来了弹窗优先级更高的弹窗时,显示更高优先级弹窗并且当前弹窗重新进入队列, PS:优先级相同时同 FGPopupViewSwitchBehaviorDiscard
FGPopupViewSwitchBehaviorAwait, //当该弹窗已经显示时,不会被后续高优线级的弹窗影响
};
通过hitTest
来解决弹窗显示条件的需求,如果根据当前的命中的弹窗没有通过hitTest
,则会根据选择的调度器策略,在当前的list中获取下一个弹窗进行测试。
- (PopupElement *)_hitTestFirstPopupResponder{
PopupElement *element;
for(auto itor=_list.begin(); itor!=_list.end();) {
PopupElement *temp = *itor;
id 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
来统一管理所以的弹窗,所以弹窗上面时候触发就需要组件自己来处理。这个笔者一共考虑了3个触发情况
- 添加弹窗对象的时候
- 通过Runloop监听主线程空闲的时刻
- 用户主动触发
通过上面3种情况,差不多已经能覆盖所有的使用场景。
另外,还给调度器添加了suspended
状态,来主动挂起/恢复弹窗队列,用来控制当前调度器是否能触发hitTest
进而展示的逻辑。
此外组件支持线程安全。考虑到操作的时机可能在任意线程,组件通过。( pthread_mutex_t
来保证线程安全pthread_mutex_t
无法切换线程上锁/解锁已经替换成信号量) 值得注意的是,弹窗的显示过程会切换到主线程进行,所以不需要去额外处理了。
至此,整个组件的业务是比较清晰了。FGPopupScheduler采用了状态模式,
组件需要让这三种处理方式可以自由的变动,所以采用策略模式来处理,下面是 UML 类图: