SwiftUI 中的弹窗问题

7,074 阅读5分钟

弹出框在项目中是随处可见的一种设计,消息提醒、操作交互、功能选择等很多需求中都会使用到。

UIKit 中,实现方式有很多,如 覆盖UIView、模态UIViewControllerkeyWindow / rootController 添加等。

今天来研究下在 SwiftUI 中有哪些实现方式。

系统自带

  • AlertActionSheet:这些系统自带弹窗就不多细说了,很方便,却不怎么支持自定义。一般在项目中用到的不会很多,毕竟大多数项目都会有自己的一套 UI 警告弹出框效果。但大多也都是参考其样式来实现的。

  • .sheet(...).fullScreenCover(...):模态常用的 Modifier,不过几乎不怎么用在弹窗上。为什么是‘几乎’,因为写到这里的时候,我特意去试了一下,可以实现,但有局限性。下面会讲到。

自定义

开始

SwiftUI 上实现一个弹窗的效果很简单。只需要用 ZStrck 嵌套俩 View 就行了。

弹窗

没错,很丑。但是确实算作是个弹窗。稍微美化一点点。再加个动画显示

代码效果

彳亍!完成了。

通过不同的 modifier 可以实现不同弹窗效果。

005XGWPvly1gmq57qgwf1g30h80zidr9 005XGWPvly1gmq57r47wig30h80zi7d6 005XGWPvly1gmq57slaw1g30h80zigu5

本文到此结束(手动狗头)

问题

如果只是用做写个 Demo 的话,上面那样就行了。

但是在实际的写项目过程中,UI 都是比较复杂的组件控件组合嵌套。尤其是在 NavigationViewTabView 以及 NavigationView + TabView 相互结合的情况下,这种效果就无法满足需求。

005XGWPvly1gmos30rvl5j31oq12449q

这并没有达到预期的要求,且在实际项目中,基本都离不开导航栏,所以问题就出现了。

我也在网上找了些类似的代码,当我给代码加上 NavigationViewTabView 后,或多或少的都出现了类似的状况。事实证明,写 Demo 和实际写项目还是会有些出入的。

解决方案

  1. 将需要弹出的 view 放在最外层

    这个解决方案是最容易想到也是最简单的。参考上面的代码,直接将 ZStack 放到 NavigationView 的外面就行了。

    005XGWPvly1gmth6hnhs6j30zg1700x5

    局限性:如果页面 A push 到了页面 B,这种方式就就会出现问题。因为 B 页面的导航栏是由 A 页面赋予的。没有办法将 ZStack 放到导航栏之外。

    一般情况下,项目的层级都是 NavigationViewTabView,如果想用用 ZStack 实现全屏弹窗的话,得将 ZStack 放到最外层。放一个两个的还可以接受,一旦弹窗多了的话,可能就比较难受了:

    005XGWPvly1gmthgz3d3sj31100dgdgk

    而且这还只是展示,没有涉及到交互。真要这么实现的话,就需要考虑一下逻辑和封装了。

    这里推荐一个:PartialSheet 就是基于最外层 View 封装的底部弹出框。感兴趣的可以参考下或者直接使用。

    建议:如果当前 View 没有或隐藏了导航栏、弹出框不需要背景等情况下。可以直接用这种方式实现。如 Hud、Toast 等。

  2. 基于SwiftUI 中的模态跳转实现

    SwiftUI 中用于模态的 Modifier 有 .sheet(...).fullScreenCover(...) (iOS 14),通过 UIViewRepresentable 给需要弹出的 View 添加 background 的方式来清除模态视图的背景色。

    005XGWPvly1gmthn3it36j31100k476r

    实现效果

    Modifier代码效果
    .fullScreenCover()005XGWPvly1gmthyjdendj31101c47ag005XGWPvly1gmthwsrph4g30h80zi12d
    .sheet()005XGWPvly1gmti3ghd6jj31101es7aq005XGWPvly1gmti82sgrog30h80zigwc

    局限性:由于使用系统模态,都是从底部动画弹出,且目前没有修改弹出动画方式。所以限制了弹窗的出现方式。

    SwiftUI 中的这两个 modifier 等同于 UIKitUIModalPresentationStyleautomaticfullScreen 的模态效果。故此种方式可以忽略项目中的层级关系,高效快速实现整体弹窗效果。

    建议:由于弹出方式受限制为底部,所以较适用于一些自定义的弹出视图。

  3. 通过获取主控制器自定义模态跳转

    有了第二个解决方案,假设一下:如果去掉了第二个解决方案的限制条件,似乎就可以解决这个问题了。

    既然 SwiftUI 中无法修改弹出方式。那就通过获取当前控制器,并修改跳转过程中的参数进行模态。

    首先通过自定义环境变量设置当前控制器:

    005XGWPvly1gmuq1jkxn3j31100qstcg

    然后修改 UIViewController 的模态跳转参数:

    005XGWPvly1gmuq8au8foj31100isgp6

    上述代码只是针对弹窗所设置的。你也可以暴露出其他参数进行自定义。由于 SwiftUIiOS 13 上没有全屏模态跳转,所以根据个人需要可以通过此方式进行自定义。

    接下来就是在 View 中实现和调用了,通过 @Environment(\.viewController) 取到主控制器,在所需要交互的地方实现调用。

    代码效果
    005XGWPvly1gmuqhhydlmj31241xgtir005XGWPvly1gmuqjsr79eg30h80zib29

    局限性:由于是模态出新的控制器,所以多个弹窗交互的间隙时间稍微长了一些,毕竟要先 dismiss 后再重新 present

    上述代码只是一个简单的显示,没有涉及到多少的功能和效果。实际写项目中会比这要复杂的多,不过只要注意多弹窗间显示或交互时候的逻辑处理,就可以处理大多数的弹窗问题。当然写起来就相对来说会复杂一些,毕竟属于整体自定义,没有借助于 SwiftUI 自带的 modifier

    建议:根据需求,简单的方法处理不了的,就用这种方式吧。

总结

弹出框问题是我在具体些项目当中遇到的,也只找到了目前的这几个解决方案。一定还有其他更好的方式没有被我发现。如果你发现上述文章或代码中有不合理的地方,欢迎留言评论。如果你有更好的解决方案,也请不吝赐教。

通常有些问题在我们写 Demo 的时候是不会发现的。而在写实际项目的时候,涉及到的逻辑和交互多了,有些问题往往就自己暴露出来了,此时通过解决问题的过程就会学习和获得更多的知识。

最后,感谢您的阅读。祝好!