iOS管理多个弹窗弹出:FGPopupScheduler

GitHub 地址:FGPopupScheduler
支持 cocopods,使用简便,效率不错的基础组件。

前言

前些天测试反馈当新用户刚打开APP的时候,由于弹窗过多,再加上还有半透明的引导层,经常会出现弹窗互相覆盖,甚至阻断正常流程的情况。而需要解决这类问题,不单单要理清楚弹窗之间的依赖关系,还需要处理弹窗本身出现的条件。并且在每次有新的弹窗加入时都需要查看之前弹窗的逻辑。每一步都要耗费开发资源。

所以我们的目的就是为了解决,如何拆分各个弹窗间的依赖关系,并在恰当地时刻依次显示弹窗,解放何时显示/何时隐藏的胶水代码。

需求分析

首先是弹窗本身的需求

  • 弹窗显示
  • 弹窗隐藏
  • 弹窗显示需要满足的条件

然后是关于弹窗与弹窗

  • 弹窗的优先级
  • 弹窗是否会受到已显示弹窗的影响

弹窗显示有一个特征,就是同一个时刻只会显示一个弹窗,并且可以是一个接一个显示。如果采用采用队列来管理的话,理所当然地就需要额外处理插入、删除、清空、遍历等行为。

这一套流程下来貌似就解决了,但实际上当把所有弹窗的统一交给一个调度器来管理的话,我们必须要考虑在什么时机显示/隐藏这些弹窗才是更加合理的。

当然,FGPopupScheduler 就能帮忙处理上面这些琐碎的事情,而且不止于此。

实现分析

考虑到弹窗本身的多样性,首先还是通过协议将队列所需要的需求抽象处理放到<FGPopupView>中,。


@protocol FGPopupView <NSObject>

@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<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来统一管理所以的弹窗,所以弹窗上面时候触发就需要组件自己来处理。这个笔者一共考虑了3个触发情况

  • 添加弹窗对象的时候
  • 通过Runloop监听主线程空闲的时刻
  • 用户主动触发
    通过上面3种情况,差不多已经能覆盖所有的使用场景。

另外,还给调度器添加了suspended状态,来主动挂起/恢复弹窗队列,用来控制当前调度器是否能触发hitTest进而展示的逻辑。

此外组件支持线程安全。考虑到操作的时机可能在任意线程,组件通过pthread_mutex_t来保证线程安全。( pthread_mutex_t无法切换线程上锁/解锁已经替换成信号量) 值得注意的是,弹窗的显示过程会切换到主线程进行,所以不需要去额外处理了。

至此,整个组件的业务是比较清晰了。FGPopupScheduler采用了状态模式,
组件需要让这三种处理方式可以自由的变动,所以采用策略模式来处理,下面是 UML 类图:

image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容