iOS项目中的弹窗管理系统实现-LMLPHP


​​​​​​​引言

在iOS项目中,弹窗是不可或缺的UI元素,尤其是在社交和娱乐类型的App中,活动通知、奖励领取、系统提示等信息常以弹窗形式呈现。然而,当项目中涉及到多种弹窗类型且数量较多时,弹窗的管理就成为了一项不可忽视的任务。多个弹窗可能会同时触发,如何确保它们有序且不打扰用户体验?这时,一个完善的弹窗管理系统便尤为重要。本文将深入探讨如何在iOS项目中实现弹窗的统一管理,涵盖弹窗的优先级设置、显示与消失机制、以及多弹窗冲突的处理等内容,为项目提供一个高效、易扩展的解决方案

组件简介及结构

本博客中介绍的的项目弹窗管理系统适用于中小型项目,弹窗总数小于500且弹窗之间没有过于复杂的互斥和交互逻辑的情况。

功能

主要功能包括:

  1. 显示弹窗和隐藏弹窗。
  2. 设置每个场景弹窗共存最大个数。
  3. 设置弹窗优先级,弹窗将按照优先级插入对应层级,当超过最大数量时自动关闭优先级最小的弹窗。
  4. 设置弹窗背景颜色及背景点击事件。

结构

组件结构:

弹窗上下文:PHDialogContext

包含了弹窗所需的一系列数据,弹窗描述、弹窗所属控制器、弹窗需要添加在的父图层,弹窗的基础优先级、弹窗的二级优先级、点击背景是否隐藏弹窗以及弹窗的背景颜色。

弹窗管理器:PHDialogManager

负责弹窗真正的显示和移除、判断弹窗是否可以显示、是否互斥、是否自动移除、插入层级等功能。

弹窗视图:PHDialogBaseView、PHDialogContainerView、PHDialogFullScreenView

  • PHDialogBaseView:弹窗的基类,所有的弹窗将会继承自它,PHDialogBaseView包含了初始化方法、供外部调用的显示和隐藏弹窗方法以及弹窗将要显示和已经显示,将要消失和已经消失的生命周期方法。
  • PHDialogContainerView:每个场景所有弹窗的唯一父图层,该视图只是继承自UIView的普通视图,用于承载弹窗,所有的弹窗将会被添加到该视图上。
  • PHDialogFullScreenView:弹窗的的背景视图,每个弹窗都会有一个对应的全屏背景视图,可以设置点击背景是否自动消失,以及背景颜色等信息。

其它文件:PHDialogDefine、PHDialogWeakViewWrapper

  • PHDialogDefine:主要用于定义全局的枚举,以及其它全局数据如最大弹窗数量。
  • PHDialogWeakViewWrapper:属于一个中间类,用于实现存放当前正在显示的弹窗视图,但又不会强持有它。

组件实现详细步骤

接下来我们开始详细介绍组件中每个结构的具体功能,以及详细的实现代码,来一步步完成这个简易的弹窗管理系统,我们先从上下文开始讨论。

弹窗上下文

PHDialogContext为弹窗的基础数据,每个弹窗在初始化时都要注入一个PHDialogContext的实例,通过上下文来定义该弹窗的优先级,弹窗需要添加到的视图或者视图控制器,以及弹窗背景和点击背景是否自动隐藏弹窗。

PHDialogContext的具体代码如下:

import UIKit

class PHDialogContext: NSObject {
    /// 弹窗描述
    var dialogDescription: String?
    /// 所属视图控制器
    weak var viewController: UIViewController?
    /// 父视图
    weak var parentView: UIView?
    /// 弹窗基础优先级(默认normal)
    var dialogBasePriority: PHDialogPriority = .normal
    /// 弹窗优先级(int类型默认为0)
    var priority: Int = 0
    /// 点击背景是否隐藏
    var isTapBackgroundDismiss: Bool = true
    /// 背景颜色
    var backgroundColor: UIColor = .black.withAlphaComponent(0.1)
    
    /// 初始化
    /// - Parameters:
    ///  - viewController: 所属视图控制器
    ///  - parentView: 父视图
    ///  - dialogBasePriority: 弹窗基础优先级
    ///  - priority: 弹窗优先级
    ///  - isTapBackgroundDismiss: 点击背景是否隐藏
    ///  - backgroundColor: 背景颜色
    ///  @note:viewController和parentView都为nil的情况下默认添加在Window上
    init(viewController: UIViewController? = nil, parentView: UIView? = nil, dialogBasePriority: PHDialogPriority = .normal, priority: Int = 0, isTapBackgroundDismiss: Bool = true, backgroundColor: UIColor = .black.withAlphaComponent(0.1)) {
        self.viewController = viewController
        self.parentView = parentView
        if self.viewController == nil && self.parentView == nil {
            self.parentView = UIApplication.shared.keyWindow
        }
        self.dialogBasePriority = dialogBasePriority
        self.priority = priority
        self.isTapBackgroundDismiss = isTapBackgroundDismiss
    }
    
}
  1. 其中的视图控制器viewController和父图层parentView传入一个即可,如果没有传递则默认添加到window上。需要注意的是这两个属性都是弱引用。
  2. 弹窗的优先级由两部分组成,一个基础优先级是一个枚举,有.low、.normal、.high三个等级,而另外一个为二级优先级,为Int类型,当基础优先级相同时,则通过priority的大小来决定优先级。
  3. 弹窗的描述可能不会用到,但为弹窗添加描述,可以让你在调试过程中更方便定位问题。
  4. 我们还为上下文创建了一个自定义的初始化方法,并设置了默认数据。

弹窗管理器

PHDialogManager是整个弹窗管理系统的核心内容,它决定了弹窗是否可以显示,弹窗是否需要自动关闭、以及掌握了弹窗的显示和关闭的具体实现方法。

属性

PHDialogManager的代码较多,我们可以把它分开来逐个方法来讨论一下,首先我们来看一下它的属性:

class PHDialogManager: NSObject {
    
    /// 单利
    static let sharedDiaglog = PHDialogManager()
    
    /// map用来存储弹窗的父视图ContainerView
    private var dialogContainerViewMap = [String: PHDialogWeakViewWrapper]()
    .....
  }
  1. 弹窗管理器被我们定义成了一个单利,意味着整个项目中只有一个弹窗管理器实例。
  2. 定义了一个字典用来存储我们已经创建的弹窗承载视图ContainerView,保证我们每个场景只创建一个ContainerView。

方法 - canShowDialog()

该方法主要用于判断目标弹窗是否可以显示,当弹窗执行show方法的时候首先会进行一些基础的判断比如是否在主线程执行,是否是横屏等等,之后就会进入canShowDialog方法来判断从其它逻辑上是否可以弹出目标弹窗,代码如下:

    /// 判断弹窗是否可以显示
    /// - Parameters:
    ///  - context: 弹窗上下文
    ///  - dialog: 弹窗
    ///  - return: 是否可以显示
    func canShowDialog(context: PHDialogContext, dialog: PHDialogBaseView) -> Bool {
        // 获取当前弹窗的parentView视图
        guard let parentView = context.parentView else {
            return false
        }
        // 获取parentView视图中的parentView视图,如果没有可以显示
        let parentViewKey = NSStringFromClass(type(of: parentView))
        // 获取父视图的ContainerView
        var containerView: PHDialogContainerView?
        if let wrapper = self.dialogContainerViewMap[parentViewKey] {
            containerView = wrapper.view as? PHDialogContainerView
        }
        // 如果没有则可以显示
        guard let containerView = containerView else { return true }
        // 如果有则获取它的subviews,有几个PHFullScreenView,
        let subViews = containerView.subviews
        // 如果大于等于最大值,则判断是否有优先级低的,如果有则显示,否则不显示
        if subViews.count < PHDialogMaxCount {
            ZMLogHelper.debug("subViews.count < PHDialogMaxCount",module: "PHDialogManager")
            return true
        }
        /// 是否可以显示 默认fasle
        var canShow = false
        for subView in subViews {
            if let fullScreenView = subView as? PHDialogFullScreenView {
                if let dialog = fullScreenView.dialog {
                    // 获取弹窗的优先级
                    let dialogBasePriority = dialog.dialogContext?.dialogBasePriority ?? .normal
                    let priority = dialog.dialogContext?.priority ?? 0
                    // 目标图弹窗优先级
                    let targetDialogBasePriority = context.dialogBasePriority ?? .normal
                    let targetPriority = context.priority ?? 0
                    // 如果目标弹窗优先级高于等于当前弹窗优先级,则可以显示
                    if targetDialogBasePriority.rawValue > dialogBasePriority.rawValue {
                        ZMLogHelper.debug("1目标弹窗优先级高于当前弹窗优先级",module: "PHDialogManager")
                        canShow = true
                        break
                    } else if targetDialogBasePriority.rawValue == dialogBasePriority.rawValue {
                        if targetPriority >= priority {
                            ZMLogHelper.debug("2目标弹窗优先级高于当前弹窗优先级",module: "PHDialogManager")
                            canShow = true
                            break
                        }
                    }
                }
            }
        }
        ZMLogHelper.info("canShow:\(canShow)",module: "PHDialogManager")
        return canShow
    }

该方法的内容较多我们逐步来看一下:

  1. 首先判断了当前的弹窗上下文是否设置了弹窗的parentView视图,也就是弹窗要显示的父图层。
  2. 第二步我们开始从获取或者创建属于该parentView视图的PHDialogContainerView实例,但需要保证相同的parentView下只有一个PHDialogContainerView实例。
  3. 获取containerView中的subviews,判断子视图的个数如果小于我们定义的最大值,那么则可以继续添加任何优先级的弹窗,直接返回true。
  4. 如果containerView中子视图的个数已经大于等于最大值,那么开始遍历containerView中的所有弹窗,找到第一个小于或者等于目标弹窗优先级的弹窗,如果有那么意味着可以显示,返回true,否则返回false。

方法 - closeDialog()

关闭当前弹窗,弹窗真正的移除代码,除了移除当前弹窗以外,还会判断当前弹窗所属的containerView中是否还有其它弹窗,如果没有也会将containerView从场景中移除。具体代码如下:

    /// 关闭弹窗
    /// - Parameters:
    /// - context: 上下文
    /// - dialog: 弹窗
    func closeDialog(context: PHDialogContext, dialog: PHDialogBaseView) {
        // 获取当前弹窗的parentView视图
        guard let parentView = context.parentView else {
            return
        }
        // 获取parentView视图中的parentView视图,如果没有可以显示
        let parentViewKey = NSStringFromClass(type(of: parentView))
        // 获取父视图的ContainerView
        var containerView: PHDialogContainerView?
        if let wrapper = self.dialogContainerViewMap[parentViewKey] {
            containerView = wrapper.view as? PHDialogContainerView
        }
        // 如果没有那就有问题了
        guard let containerView = containerView else {
            assert(false, "PHDialogManager: parentView has been added to another containerView.")
            return
        }
        // 如果有则获取它的subviews,有几个PHFullScreenView,
        let subViews = containerView.subviews
        if subViews.count == 0 {
            assert(false, "PHDialogManager: parentView has been added to another containerView.")
            return
        }
        if subViews.count == 1 {
            // 直接连同Container移除
            containerView.removeFromSuperview()
            self.dialogContainerViewMap.removeValue(forKey: parentViewKey)
            return
        }
        if let fullScreenView = dialog.superview as? PHDialogFullScreenView {
            fullScreenView.removeFromSuperview()
        } else {
            //弹窗的父视图不是PHFullScreenView
            assert(false, "PHDialogManager: dialog's superview is not PHFullScreenView.")
        }
        
    }
  1. 首先会判断弹窗是否有parentView视图,如果没有则代表弹窗已经被移除了。
  2. 获取到parentView视图之后,会根据它来获取该场景的PHDialogContainerView实例。
  3. 如果PHDialogContainerView实例中只有一个子视图则直接移除containerView视图。
  4. 如果有多个视图则移除我们当前要移除的目标弹窗。

方法 - showDialog()

显示弹窗时的实际执行代码,该方法不仅仅负责显示弹窗,创建containerView视图,以及弹窗的插入和自动消失逻辑都在该方法里进行,具体代码如下:

    /// 显示弹窗
    /// - Parameters:
    ///  - dialog: 弹窗
    func showDialog(targetDialog: PHDialogBaseView) {
        // 获取弹窗相关的信息
        guard let context = targetDialog.dialogContext else {
            return
        }
        // 获取要添加的父视图
        guard let parentView = context.parentView else {
            return
        }
        let parentViewKey = NSStringFromClass(type(of: parentView))
        // 获取父视图的ContainerView
        var containerView: PHDialogContainerView?
        if let wrapper = self.dialogContainerViewMap[parentViewKey] {
            containerView = wrapper.view as? PHDialogContainerView
        }
        if containerView == nil {
            containerView = PHDialogContainerView(frame: UIScreen.main.bounds)
            parentView.addSubview(containerView!)
            self.dialogContainerViewMap[parentViewKey] = PHDialogWeakViewWrapper(view: containerView!)
        }
        if parentView != containerView?.superview {
            // 同一个父视图下只能有一个ContainerView
            assert(false, "PHDialogManager: parentView has been added to another containerView.")
            return
        }
        
        guard let containerView = containerView else {
            return
        }
        /**优先级越大越在上面**/
        
        // 获取弹窗
        let subViews = containerView.subviews
        if subViews.count == 0 {
            // 直接添加
            addDialog(dialog: targetDialog, containerView: containerView, insertIndex: 0)
        } else {
            // 最大弹窗数
            let maxDialogCount = PHDialogMaxCount
            // 需要插入的位置索引
            var insertIndex = subViews.count
            for (index, subView) in subViews.enumerated() {
                if let fullScreenView = subView as? PHDialogFullScreenView {
                    if let dialog = fullScreenView.dialog {
#if DEBUG
                        if let homeDialog = dialog as? ZMHomeWelcomeDialog {
                            ZMLogHelper.debug("dialog:\(homeDialog.index ?? 0)",module: "PHDialogManager")
                        }
#endif
                        // 获取弹窗的优先级
                        let dialogBasePriority = dialog.dialogContext?.dialogBasePriority ?? .normal
                        let priority = dialog.dialogContext?.priority ?? 0
                        
                        // 目标图弹窗优先级
                        let targetDialogBasePriority = targetDialog.dialogContext?.dialogBasePriority ?? .normal
                        let targetPriority = targetDialog.dialogContext?.priority ?? 0
                        
                        ZMLogHelper.debug("dialogBasePriority:\(dialogBasePriority.rawValue) priority:\(priority) targetDialogBasePriority:\(targetDialogBasePriority.rawValue) targetPriority:\(targetPriority)",module: "PHDialogManager")
                        
                        // 高级优先级相同
                        if targetDialogBasePriority.rawValue == dialogBasePriority.rawValue {
                            // 优先级大于等于
                            if targetPriority < priority {
                                ZMLogHelper.debug("1发现优先级大于目标弹窗的弹窗",module: "PHDialogManager")
                                insertIndex = index
                                break
                            }
                        } else if targetDialogBasePriority.rawValue < dialogBasePriority.rawValue {
                            ZMLogHelper.debug("2发现优先级大于目标弹窗的弹窗",module: "PHDialogManager")
                            insertIndex = index
                            break
                        }
                    }
                }
            }
            // 添加弹窗
            addDialog(dialog: targetDialog, containerView: containerView, insertIndex: insertIndex)
            // 移除多余最大值的弹窗
            let subviews = containerView.subviews
            if subviews.count > maxDialogCount {
                removeDialog(subviews: subviews)
            }
        }
    }

该方法中的内容较多,我们想将重点放到显示弹窗上面:

  1. 首先进行了判断是否注入了弹窗的上下文,以及是否设置了父图层。
  2. 获取当前场景中唯一PHDialogContainerView实例,如果没有则手动创建并存放到缓存中。
  3. 获取到了containerView实例之后,需要保证同一个parentView下只能用有一个containerView实例。
  4. 获取containerView上面的子视图,也就是添加弹窗的PHDialogFullScreenView类的实例。
  5. 如果containerView尚未添加任何弹窗,则直接执行添加方法。
  6. 否则开始循环遍历containerView的subViews,获取每个弹窗的优先级与目标弹窗进行比较,找到第一个优先级大于目标弹窗优先级的弹窗,并插入到它的下面。
  7. 判断该场景中当前弹窗的总数是否大于弹窗最大数,如果大于则执行移除操作。

方法 - addDialog()

该方法用于添加或者插入弹窗,计算好目标弹窗要插入的位置后执行该方法来添加一个新的的弹窗,代码如下:

    /// 添加弹窗到指定位置
    /// - Parameters:
    /// - dialog: 弹窗
    /// - containerView: 父视图
    /// - insertIndex: 插入位置
    private func addDialog(dialog: PHDialogBaseView, containerView: PHDialogContainerView, insertIndex: Int) {
        // 创建全屏视图
        let fullScreenView = PHDialogFullScreenView(frame: containerView.bounds)
        fullScreenView.dialog = dialog
        fullScreenView.addSubview(dialog)
        // 如果不是第一个弹窗,插入到指定位置
        let subViews = containerView.subviews
        if insertIndex < subViews.count {
            ZMLogHelper.debug("插入弹窗",module: "PHDialogManager")
            let targetView = subViews[insertIndex]
            containerView.insertSubview(fullScreenView, belowSubview: targetView)
        } else {
            containerView.addSubview(fullScreenView)
        }
        
    }
    
  1. 首先为弹窗创建一个全屏的背景视图PHDialogFullScreenView的实例,并添加弹窗到fullScreenView上
  2. 判断需要插入的index是否小于containerView上的弹窗总数,如果小于则插入到指定为止。
  3. 如果index大于或者等于containerView上的弹窗总数,则直接添加。

方法 - removeDialog()

该方法用于当弹窗发生互斥,数量超过最大值时,自动移除弹窗,代码如下:

    /// 移除多余最大值的弹窗
    /// - Parameters:
    /// - subviews: 子视图
    private func removeDialog(subviews: [UIView]) {
        // 最大弹窗数
        let maxDialogCount = PHDialogMaxCount
        if subviews.count > maxDialogCount {
            if let fullScreenView = subviews.first as? PHDialogFullScreenView {
                if let dialog = fullScreenView.dialog {
                    ZMLogHelper.debug("移除弹窗 \(dialog.dialogContext?.dialogDescription ?? "")",module: "PHDialogManager")
                    dialog.closeDialog()
                    removeDialog(subviews: Array(subviews.dropFirst()))
                }
            }
        }
    }
  1. 判断弹窗总数是否大于最大值。
  2. 获取场景中的优先级最低的弹窗,执行关闭操作。
  3. 递归执行该方法。

弹窗视图

我们一共创建了三个视图但视图的内容相对简单,因为逻辑部分我们都集中到了弹窗管理器。

PHDialogBaseView

该视图是所有弹窗视图的基类,之后创建的弹窗需要继承自它,弹窗定义了基本的属性以及公共方法。

属性

包含了一个弹窗上下文,以及它的父图层,还有布尔类型的弹窗是否正在显示的标记。

    /// 弹窗上下文
    var dialogContext: PHDialogContext?
    /// 全屏的视图,弹窗会放到这个视图上
    weak var fullScreenView: PHDialogFullScreenView?
    /// 是否正在显示
    var isShowing: Bool = false
  1. 上下文会以依赖注入的方式传入到弹窗中。
  2. 弹窗的背景视图,方便我们从弹窗中直接获取背景视图。
  3. isShowing属性标记了弹窗的显示状态。
自定义初始化

该方法传入了弹窗的上下文模型。

    init(dialogContext: PHDialogContext) {
        self.dialogContext = dialogContext
        super.init(frame: CGRect.zero)
    }
生命周期方法

这一些列的方法用于追踪弹窗的生命周期,从显示到隐藏。

    /// 显示弹窗
    /// - Returns: 是否显示成功
    func showDialog() -> Bool {
        // 是否在主线程执行
        if !Thread.isMainThread {
            assert(false, "PHDialogBaseView: showDialog() must be called on the main thread.")
            return false
        }
        // 是否还没调用显示方法就已经被添加在了视图上
        if self.superview != nil && self.isShowing == false {
            assert(false, "PHDialogBaseView: showDialog() has been called.")
            return false
        }
        // 横屏不显示
        if UIApplication.shared.statusBarOrientation.isLandscape {
            return false
        }
        // 未设置上下文
        guard let context = self.dialogContext else {
            assert(false, "PHDialogBaseView: dialogContext is nil.")
            return false
        }
        
        var isCanShow = false
        if PHDialogManager.sharedDiaglog.canShowDialog(context: context, dialog: self) {
            dialogWillShow()
            isCanShow = true
            PHDialogManager.sharedDiaglog.showDialog(targetDialog: self)
        }
        isShowing = true
        dialogDidShow()
        return isCanShow
    }
    
    /// 关闭弹窗
    func closeDialog() {
        // 弹窗上下文
        guard let context = self.dialogContext else {
            assert(false, "PHDialogBaseView: dialogContext is nil.")
            return
        }
        // 是否在主线程执行
        if !Thread.isMainThread {
            assert(false, "PHDialogBaseView: closeDialog() must be called on the main thread.")
            return
        }
        dialogWillClose()
        // 关闭弹窗
        PHDialogManager.sharedDiaglog.closeDialog(context: context, dialog: self)
        isShowing = false
        dialogWillClose()
    }
    
    /// 弹窗 将要显示
    func dialogWillShow() {
     // 子类实现
    }
    
    /// 弹窗 已经显示
    func dialogDidShow() {
     // 子类实现
    }
    
    /// 弹窗 将要关闭
    func dialogWillClose() {
     // 子类实现
    }
    
    /// 弹窗 已经关闭
    func dialogDidClose() {
     // 子类实现
    }

PHDialogFullScreenView

此视图是弹窗的背景视图,主要用于设置背景颜色,以及弹窗背景的点击事件。

import UIKit

class PHDialogFullScreenView: UIView {
    
    /// 弹窗
    weak var dialog:PHDialogBaseView? {
        didSet {
            self.backgroundColor = dialog?.dialogContext?.backgroundColor
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = .black.withAlphaComponent(0.1)
        addTapGesture()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func addTapGesture() {
        let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction))
        self.isUserInteractionEnabled = true
        self.addGestureRecognizer(tap)
    }
    
    @objc func tapAction() {
        if dialog?.dialogContext?.isTapBackgroundDismiss == true {
            dialog?.closeDialog()
        }
    }

}

PHDialogContainerView

此视图为每个场景中的弹窗父图层不需要任何特殊处理。同一场景,所有的弹窗都将会被添加到同一个PHDialogContainerView实例。

class PHDialogContainerView: UIView {


}

其它文件

PHDialogDefine主要负责定义全局的枚举和常量,代码如下:

/// 弹窗优先级
public enum PHDialogPriority: Int {
    case low = 0
    case normal = 1
    case high = 2
}

/// 弹窗共存最大数量
public let PHDialogMaxCount = 5

PHDialogWeakViewWrapper中间类,主要用于弱引用弹窗视图进行缓存。

import UIKit

class PHDialogWeakViewWrapper: NSObject {
    
    weak var view: UIView?

    init(view: UIView) {
        self.view = view
    }
}

弹窗的使用

接下来我们就在实际项目中来使用弹窗,并且根据需要来设置不同优先级的弹窗来查看一下我们已经实现的效果。

创建弹窗

首先我们继承自PHDialogBaseView先来创建一个简单的弹窗,弹窗中会包含一个标题以及一个内容文案。另外弹窗还会包含两个按钮,添加按钮点击后会弹出新的弹窗。而关闭按钮点击后会直接关闭当前弹窗。

具体代码如下:

import UIKit

class ZMHomeWelcomeDialog: PHDialogBaseView {

    /// 弹窗标题
    private let titleLabel = UILabel()
    /// 弹窗内容
    private let contentLabel = UILabel()
    /// 添加按钮
    private let addButton = UIButton()
    /// 关闭按钮
    private let closeButton = UIButton()
    /// 添加弹窗回调
    var addActionBlock: (() -> Void)?
    
    /// 弹窗序号
    var index:Int? {
        didSet {
            titleLabel.text = "这是第\(index ?? 0)个弹窗"
        }
    }
    
    
    
    override init(dialogContext: PHDialogContext) {
        super.init(dialogContext: dialogContext)
        setupView()
        setLayout()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupView() {
        // 标题
        self.addSubview(titleLabel)
        titleLabel.text = "这是标题"
        titleLabel.textColor = ZMColorHelper.titleColor
        titleLabel.font = UIFont.systemFont(ofSize: 18)
        
        // 内容
        self.addSubview(contentLabel)
        contentLabel.text = "这是内容撒快递哈电话卡汕德卡双打卡久啊手打卡莎阿拉斯加大数据大石街道"
        contentLabel.numberOfLines = 0
        contentLabel.textColor = ZMColorHelper.subTitleColor
        
        // 添加按钮
        self.addSubview(addButton)
        addButton.setTitle("添加", for: .normal)
        addButton.setTitleColor(ZMColorHelper.titleColor, for: .normal)
        addButton.titleLabel?.font = UIFont.systemFont(ofSize: 16)
        addButton.addTarget(self, action: #selector(addAction), for: .touchUpInside)
        
        // 关闭按钮
        self.addSubview(closeButton)
        closeButton.setTitle("关闭", for: .normal)
        closeButton.setTitleColor(ZMColorHelper.titleColor, for: .normal)
        closeButton.titleLabel?.font = UIFont.systemFont(ofSize: 16)
        closeButton.addTarget(self, action: #selector(closeAction), for: .touchUpInside)
        
    }
    
    private func setLayout() {
        
        titleLabel.snp.makeConstraints { make in
            make.top.equalTo(20)
            make.centerX.equalToSuperview()
        }
        
        contentLabel.snp.makeConstraints { make in
            make.top.equalTo(titleLabel.snp.bottom).offset(10)
            make.left.equalTo(20)
            make.right.equalTo(-20)
        }
        
        // 添加按钮
        addButton.snp.makeConstraints { make in
            make.top.equalTo(contentLabel.snp.bottom).offset(20)
            make.centerX.equalToSuperview().offset(-40)
            make.width.equalTo(80)
            make.height.equalTo(40)
        }
        
        // 关闭按钮
        closeButton.snp.makeConstraints { make in
            make.top.equalTo(contentLabel.snp.bottom).offset(20)
            make.centerX.equalToSuperview().offset(40)
            make.width.equalTo(80)
            make.height.equalTo(40)
            make.bottom.equalTo(-20)
        }
    }
    
    @objc private func addAction() {
        addActionBlock?()
    }
    
    @objc private func closeAction() {
        self.closeDialog()
    }

}

显示弹窗

在一个视图控制器中使用我们刚刚创建的弹窗,为了测试弹窗管理系统的功能我们首先定义了一个Int类型的index,以及一个存放整形优先级的数组,用来给弹窗设置不同的优先级。

    private var index = 0
   @objc private func testAction() {
        /// 弹窗优先级
        let prioritys = [10,20,30,15,40,5,60,10,90]
        
        let context = PHDialogContext()
        context.priority = prioritys[index%prioritys.count]
        context.dialogDescription = "测试弹窗\(context.priority)"
        let dialog = ZMHomeWelcomeDialog(dialogContext: context)
        dialog.index = prioritys[index%prioritys.count]
        dialog.backgroundColor = .white
        dialog.layer.masksToBounds = true
        dialog.layer.cornerRadius = 10
        let result = dialog.showDialog()
        if result {
            dialog.snp.makeConstraints { make in
                make.center.equalToSuperview()
                make.width.equalTo(300.0)
            }
        }
        
        dialog.addActionBlock = {[weak self] in
            guard let self = self else { return }
            self.index = self.index + 1
            self.testAction()
        }
        
    }
 
  1. 每个弹窗创建时会设置不同的优先级以及弹窗描述。
  2. 在弹窗的添加事件回调中,会将index+1并弹出新的弹窗。

弹窗层级

弹窗的弹窗层级结构中会有三个主要的视图,一个是PHDialogBaseView也就是弹窗视图,一个是PHDialogFullScreenView弹窗的半透明背景视图,另外一个是PHDialogContainerView用于在场景中添加弹窗的父视图,每个场景中的PHDialogContainerView视图都应该是唯一的,而PHDialogBaseView和PHDialogFullScreenView的个数则为弹窗个数。

iOS项目中的弹窗管理系统实现-LMLPHP

覆盖

当点击添加弹窗时,第二个和第三个弹窗优先级分别为20和30则应该直接覆盖在原来的弹窗之上,我们来看一下执行后的视图结构。

iOS项目中的弹窗管理系统实现-LMLPHP

可以看见结果和我们预想的是相同的,优先级大的弹窗会覆盖在优先级小的弹窗之上。

插入

第四个弹窗的优先级为15,大于第一个弹窗但是小于后两个弹窗,所以在添加第四个弹窗时,它应该会被插入到第一个优先级大于它的弹窗的下面。

执行代码我们可以看见下面的日志

再来看一下结构图如下:

iOS项目中的弹窗管理系统实现-LMLPHP

可以看见弹窗被插入到了优先级10与20之间。

拦截

当我们添加第五个弹窗时,由于弹窗优先级大于所有弹窗所以一定会被直接添加。但当我们添加第6个弹窗时,由于已经达到了弹窗的最大值,且当前目标弹窗的优先级小于所有正在显示弹窗的优先级,所以该弹窗会被拦截,不能显示。

执行代码后会看见下面日志:

并且弹窗不会弹出。

iOS项目中的弹窗管理系统实现-LMLPHP

互斥

当添加第7个弹窗时,由于它的优先级大于所有弹窗则系统会关闭正在显示弹窗中优先级最低的弹窗,并且添加新的弹窗到指定层级。

日志如下:

层级效果如下:

iOS项目中的弹窗管理系统实现-LMLPHP

场景中只剩下优先级最大的5个弹窗。

结语

弹窗管理系统在iOS项目中起着至关重要的作用,特别是在弹窗频繁出现的应用场景中,合理的弹窗管理能够极大提升用户的使用体验。通过本文介绍的方法,我们实现了弹窗的统一管理,从优先级设置到冲突处理,确保了弹窗的有序呈现和及时消失。一个完善的弹窗管理系统不仅帮助我们应对当前的需求,还为未来的功能扩展打下了良好基础。在实际项目中,合理运用这些管理技巧,相信能够让弹窗成为用户体验的加分项。

11-16 15:23