iOS window弹窗管理

App里总会有很多的弹窗,为了美观,大多数弹窗都需要盖住导航栏;这时弹窗会添加到window上以满足需求。但添加到window上的弹窗却不方便管理,也与页面脱离关系,如果有异步的情况,弹窗会更加复杂难以处理,如何才能对window弹窗统一进行管理,解决这些问题?

遇到的问题

我们一般在开发的过程中要进行一个弹窗展示通常第一步会创建一个自定义View来编写弹窗的具体UI页面,然后通过实现一个基础的动画来将其添加到我们的当前的Window上。

在项目规模较小或者业务不复杂的时候,这样写可能更简单一点,但是慢慢的你会发现,业务中弹窗多了之后。每新增一个弹窗不光要单独造一个弹窗出来,如果业务当前逻辑中有和其他地方的弹窗有依赖,那么还要考虑其他地方的弹窗是否有冲突或者谁先展示的问题。
面临难维护的问题:

  1. 多个弹窗可能会产生重叠:如app启动的时候有2个弹窗,正巧2个弹窗都触发展示,这时候这2个弹窗就会重叠在一起。
  2. 弹窗无法与页面关联:如登录后有个弹框需要在tab2页显示,但是启动后首屏页为tab1,这时候弹框就不能显示,当tab2出现时才显示。
  3. 弹窗无法设置优先级:一个比较简单的例子:有多个弹层的新手引导,用户关闭引导页1时,按顺序呈现引导页2、引导页3, 如果中间有其他弹窗出现的逻辑,应该等待引导页结束再展示。
  4. 弹窗无法留活:一个简单的例子:一个活动弹窗含有2个活动,点击活动A进入详情页,此时window弹窗应该消失,当从详情页返回时,活动弹窗应该继续展示,才能点击进入活动B查看详情。为了避免重新触发弹窗的逻辑,应该对弹窗进行缓存。
  5. 异步弹窗处理复杂:例如网络请求弹窗的数据,弹窗的展示因此延时,用户在此期间跳转其他页面,或者当前页面已经返回,因为是window弹窗,弹窗则不应该显示出来。
  6. 弹窗不能自动关闭:例如用户被迫下线,此时app的所有弹窗都应该自动移除,或者弹窗展示情况下app发生页面跳转,避免弹窗忘记关闭的情况,也应该自动移除现有的弹窗。

问题分析

多个弹窗可能会产生重叠

这个比较好解决,主要思路是用一个manager统一管理显示和隐藏方法, 再利用队列进行记录保存等待显示的页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+(void)show:(UIView*)view{
if
// 当前无弹窗展示直接展示
else
// 当前有弹窗则加入队列中
}

+(void)dismiss:(UIView*)view{
if view 正在展示
隐藏view
else
从队列中移除view

然后开始显示队列中下一个view
}

弹窗无法与页面关联

  1. 为弹窗页面声明统一的协议,目前协议中添加要关联的信息(UIViewController类名)
  2. manager中 show/dismiss方法 需要传入当前显示页面的额外参数
  3. 目前可以明确manager的 show/dismiss方法需要有UIViewController的显示 隐藏来调度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//hook UIViewController的viewDidAppear方法,通知manager准备展示队列中下一个view
//hook UIViewController的viewDidDisAppear方法,通知manager隐藏当前UIViewController绑定的弹窗
- (void)p_viewWillAppear:(BOOL)animated {
[self p_viewWillAppear:animated];
[[NSNotificationCenter defaultCenter] postNotificationName:LYViewControllerViewWillAppearNotification object:@{LYViewControllerClassName:NSStringFromClass([self class]),LYViewControllerClassIdentifier:self}];
}

- (void)p_viewDidAppear:(BOOL)animated {
[self p_viewDidAppear:animated];
[[NSNotificationCenter defaultCenter] postNotificationName:LYViewControllerViewDidAppearNotification object:@{LYViewControllerClassName:NSStringFromClass([self class]),LYViewControllerClassIdentifier:self}];
}

- (void)p_viewWillDisappear:(BOOL)animated {
[self p_viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter] postNotificationName:LYViewControllerViewWillDisappearNotification object:@{LYViewControllerClassName:NSStringFromClass([self class]),LYViewControllerClassIdentifier:self}];
}

- (void)p_viewDidDisappear:(BOOL)animated {
[self p_viewDidDisappear:animated];
[[NSNotificationCenter defaultCenter] postNotificationName:LYViewControllerViewDidDisappearNotification object:@{LYViewControllerClassName:NSStringFromClass([self class]),LYViewControllerClassIdentifier:self}];
}

三、设置弹窗优先

因为现在有了弹窗等待队列,弹窗的优先级也就可以很好的解决,在添加进队列时,给弹窗设置一个level值,根据level值排序后从队列里推出展示的弹窗自然是优先级比较高的弹窗。
  因为有时候无法确认其他弹框的level值,level的设定建议以场景来设置level,因为同一场景的多个弹窗大部分情况下无需按优先级展示,同等level能按先后顺序展示即可。

四、弹窗无法留活:

还是之前抛出的问题,点击活动A进入详情页,此时window弹窗应该消失,当从详情页返回时,活动弹窗应该继续展示。为了避免再一次执行弹窗的展示逻辑,所以需要对当前的弹窗进行缓存,等待页面重新回来时展示。 这种情况只是页面暂时离开,页面并未从页面路径栈里消失,如果页面已经不存在,那么缓存里的弹窗也应该移除。
  新建一个弹框的缓存数组,这里并没有放入之前等待队列里, 是因为等待队列里的弹窗都是仍未展示的,无论页面是否新建(根据class),当这个页面是弹窗指定的归属类时都可以展示出来。而缓存的弹窗是与具体的页面关联的(根据obj),如果页面返回再重新进入,页面已经重新构造,上次缓存的弹窗是不应该再展示的,因为页面重新构造后可能会重新触发弹窗的逻辑,这时候可能就会2个相同的弹窗,所以这里用了2个队列存储弹框。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
+ (void)viewNeedShowFromQueueWithPage:(UIViewController *)page {
if([NSStringFromClass(page.class) isEqualToString:@"PopoMessageListViewController"]){

}
// NTLog(@" NTWindowPop %s %p page:%@",__func__,self,page);
if (!page || ![page isKindOfClass:[UIViewController class]]) {
return;
}

UIViewController * rootvc = [UIApplication sharedApplication].delegate.window.rootViewController;
if(![rootvc isKindOfClass:NSClassFromString(@"POPOTabbarController")]){
return;
}
// NTLog(@" NTWindowPop %s %p rootvc:%@",__func__,self,rootvc);

// 判断当前页是否有存活的弹框,有则加入队列中。
if ([self shareInstance].arrayAliveViews.count) {
for (NSInteger i = [self shareInstance].arrayAliveViews.count-1; i >= 0 ; i--) {
NTWindowPopViewItem *model = [self shareInstance].arrayAliveViews[i];
if (page == model.identifier) {
[[self shareInstance].arrayWaitViews addObject:model];
[[self shareInstance].arrayAliveViews removeObject:model];
}
}
}

//当前在显示升级弹窗
if([self isShowVersionUpdate]){
return;
}

// 当前屏幕有弹框,则不显示
if ([self shareInstance].currentView) {
return;
}
// 队列里无等待显示的视图
if (![self shareInstance].arrayWaitViews.count) {
return;
}

// 有指定页面,但页面实例发生变化,都清除
for (NSInteger i = [self shareInstance].arrayWaitViews.count-1; i >= 0; i--) {
NTWindowPopViewItem *obj = [self shareInstance].arrayWaitViews[i];
if ([self isMemberOfClass:page.class pages:obj.pagesClass]) {
if (obj.bindPage && (!obj.controller || obj.controller != page)) {
[[self shareInstance].arrayWaitViews removeObject:obj];
[self removeNotification];
}
}
}

// 重新根据优先级排列队列
[[self shareInstance].arrayWaitViews sortUsingComparator:^NSComparisonResult(NTWindowPopViewItem *obj1, NTWindowPopViewItem * obj2) {
return obj1.level <= obj2.level ? NSOrderedDescending : NSOrderedAscending;
}];

//所有弹窗都不展示
if (NTWindowPop.hideAllAlert) {
return;
}
// NTLog(@" NTWindowPop %s %p rootvc:%@",__func__,self,rootvc);
// 推出队列中需要展示的视图进行展示
__block NTWindowPopViewItem *model = nil;
NSMutableArray * arrs = [self shareInstance].arrayWaitViews;
for (int i = 0; i < arrs.count; i++) {
NTWindowPopViewItem *obj = arrs[i];
// NTLog(@" NTWindowPop %s %p index:%d obj.view:%@",__func__,self,i,obj.view);
}
[arrs enumerateObjectsUsingBlock:^(NTWindowPopViewItem *obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (obj.pagesClass && obj.view) {
if ([self isMemberOfClass:page.class pages:obj.pagesClass]) {
UIWindow *window = [UIApplication sharedApplication].delegate.window;
// NSAssert(window, @"检查window");
[window addSubview:obj.view];
// NTLog(@" NTWindowPop %s %p window:%@ window:%@ obj.view:%@",__func__,self,window,obj.view);
[self shareInstance].keepAlive = obj.keepAlive;
[self shareInstance].pagesClass = obj.pagesClass;
[self shareInstance].currentView = obj.view;
[self shareInstance].showCompleted = obj.showCompleted;
if ([self shareInstance].showCompleted) {
[self shareInstance].showCompleted();
}
model = obj;
*stop = YES;
}
}
}];
if (model) {
[[self shareInstance].arrayWaitViews removeObject:model];
[self removeNotification];
}
}

五、异步弹窗情况:

异步弹框的情况稍微复杂,基本上都会跟网络请求扯上联系,如果网络请求未完成的情况下,频繁的“进入-返回”,这可能会出现多个弹窗的网络请求同时在请求

对于这种情况,可以在初始比如首页初始化的地方 将所有弹窗view添加到manager中。
添加到队列中直接检查view是否需要请求异步数据,若需要请求异步数据,则开始请求,并将view添加到等待队列中。
在请求完成后自动添加回待显示队列。

六、自动删除弹窗:

有些app需要登录之后才能展示弹窗,如果用户下线或者被踢,这个用户的弹窗都应该移除。 因为有了队列,当用户下线时移除当前展示的弹窗和队列里等待弹窗就可以统一移除manager管理的所有弹窗。