如何设计一个优雅的弹框,Swift版

理念:爽到使用者就行了

最爽的 调用弹框,那肯定是:Alert().show()
什么,你要手动隐藏?那我再提供一个 dismiss()函数吧。
什么,你还要改背景颜色,弹框动画的方向,还要能自定义……

思路:通过协议的方式,提供默认实现

行行行,都满足你。我提供一个协议给你,然后帮你实现默认的动画,剩下的你自己发挥想象力就好了。

协议(AlertProtocol.swift):
public enum AppearFrom {
    case top, bottom, left, right
}

// Protocol for showing and dissmising alert view
public protocol AlertProtocol {
    func show(animated:Bool) -> Self
    func dismiss(animated:Bool) -> Self
    var backgroundView: UIView {get}
    var dialogView: UIView {get set}
    var appearFrom: AppearFrom {get}
    var clearBackground: Bool {get}
}

extension AlertProtocol {
    @discardableResult
    public func show() -> Self {
        return show(animated: true)
    }
    @discardableResult
    public func dismiss() -> Self {
        return dismiss(animated: true)
    }
}
默认实现 showdismiss

extension AlertProtocol where Self: UIView{
    
    @discardableResult
    public func show(animated: Bool) -> Self {
        return show(animated: animated, superview: nil)
    }
    
    @discardableResult
    public func show(animated: Bool, superview: UIView?) -> Self {
        
        // Set origin before Animation
        if appearFrom == .top {
            self.dialogView.center = CGPoint(x:self.center.x, y:-self.frame.height+self.dialogView.frame.size.height/2)
        }else if appearFrom == .bottom {
            self.dialogView.center = CGPoint(x:self.center.x, y:self.frame.height+self.dialogView.frame.size.height/2)
        }else if appearFrom == .left {
            self.dialogView.center = CGPoint(x:-self.frame.size.width, y:self.frame.height/2)
        }else {
            self.dialogView.center = CGPoint(x:self.frame.size.width, y:self.frame.height/2)
        }
        
        self.backgroundView.alpha = 0
        self.backgroundView.isHidden = false
        if superview != nil {
            superview?.addSubview(self)
        }else {
            let cv = UIViewController.currentViewController()
            if let nav = cv?.navigationController {
                nav.view.addSubview(self)
            }else {
                cv?.view.addSubview(self)
            }
        }
        if animated {
            
            UIView.animate(withDuration: 0.33, animations: {
                if self.clearBackground == true{
                    self.backgroundView.alpha = 0
                }else{
                    self.backgroundView.alpha = 0.6
                }
            })
            // Set origin during Animation
            UIView.animate(withDuration: 0.33, delay:0, usingSpringWithDamping:0.7, initialSpringVelocity:10, options:UIView.AnimationOptions(rawValue:0), animations: {
                self.dialogView.center = self.center
            })
        }else{
            if self.clearBackground == true{
                self.backgroundView.alpha = 0
            }else{
                self.backgroundView.alpha = 0.6
            }
            self.dialogView.center = self.center
        }
        return self
    }
    
    @discardableResult
    public func dismiss() -> Self {
        return dismiss(animated: true)
    }
    
    @discardableResult
    public func dismiss(animated: Bool) -> Self {
        
        self.backgroundView.isHidden = true
        if animated {
            UIView.animate(withDuration: 0.33, animations: {
                self.backgroundView.alpha = 0
            })
            
            UIView.animate(withDuration: 0.33, delay:0, usingSpringWithDamping: 1, initialSpringVelocity:10, options:UIView.AnimationOptions(rawValue:0), animations: {
                
                if self.appearFrom == .top {
                    self.dialogView.center = CGPoint(x:self.center.x, y:-self.frame.height+self.backgroundView.frame.size.height/2)
                }else if self.appearFrom == .bottom {
                    self.dialogView.center = CGPoint(x:self.center.x, y:self.frame.height+self.dialogView.frame.size.height/2)
                }else if self.appearFrom == .left {
                    self.dialogView.center = CGPoint(x:-self.frame.size.width, y:self.frame.height/2)
                }else {
                    self.dialogView.center = CGPoint(x:self.frame.size.width+self.dialogView.frame.size.width, y:self.frame.height/2)
                }
                
                
            }) { (completed) in
                self.removeFromSuperview()
            }
            
        }else{
            self.removeFromSuperview()
        }
        return self
    }
}

extension UIViewController {
    /// 获取当前最顶层的vc
    internal class func currentViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
        if let nav = base as? UINavigationController {
            return currentViewController(base: nav.visibleViewController)
        }
        if let tab = base as? UITabBarController {
            return currentViewController(base: tab.selectedViewController)
        }
        if let presented = base?.presentedViewController {
            return currentViewController(base: presented)
        }
        return base
    }
    
}

进化:提供一个容器

使用者吐槽,你在逗我吗?这玩意怎么用啊,你这也太敷衍了吧,我还要写一大堆代码,才能用。
好吧,那我给你一个默认的容器吧,你只要告诉我中间显示啥就可以了。
调用1:

let base = OrderPayView.nibView().setTotalL("1000")
let al = AlertCustomView(base)
    .show(animated: true)

base.payBtn.rx.tap.bind { [unowned al] in
    al.dismiss()
    print("pay click")
}.disposed(by: bag)

调用2:

let base = UILabel()
base.text = "1000"
AlertCustomView(base, false).show(animated: true)

效果:


效果1

效果2

容器代码(AlertCustomView.swift):

/// 自定义弹框
open class AlertCustomView: UIView, AlertProtocol {
     
    /// 自定义的组件
    open var base: Base {
        get { return customView }
        set { customView = newValue }
    }
    
    // MARK: - datas
    public var appearFrom: AppearFrom = .right
    public var clearBackground = Bool()
    
    public var closeBtnClick: (() -> Void)?
    
    // MARK: - views
    public var backgroundView = UIView()
    public var dialogView = UIView()
    // 自定义的View
    fileprivate var customView: Base = Base()
    /// 是否隐藏close
    private var isHiddenClose: Bool = true
    private var closeBtn: UIButton! // 关闭
    
    // MARK: - leftStyle
    deinit {
        print("alertType deinit")
    }
    
    fileprivate override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initViews()
        latoutUI()
    }
    
    public convenience init(_ customView: Base, _ isHiddenClose: Bool = true) {
        
        self.init(frame: UIScreen.main.bounds)
        self.customView = customView
        self.isHiddenClose = isHiddenClose
        initViews()
        latoutUI()
    }
    
    // MARK: - events
    
    // dismiss the alert view
    @objc public func didcloseBtnTapped() {
        dismiss(animated: true)
        closeBtnClick?()
    }
    
    @objc func backgroundViewTap() {
        print("backgroundViewTap")
    }
    
    fileprivate func latoutUI() {
        let cf = base.frame
        
        backgroundView.frame = frame
        addSubview(backgroundView)
        
        addSubview(dialogView)
        dialogView.addSubview(customView)
        dialogView.addSubview(closeBtn)
        
        dialogView.snp.makeConstraints { (make) in
            make.center.equalToSuperview()
        }
        
        customView.snp.makeConstraints { (make) in
            make.size.equalTo(cf.size).priority(.low)
            
            let bottom: CGFloat = isHiddenClose ? 0 : 54.5
            make.edges.equalTo(UIEdgeInsets(top: cf.origin.y, left: cf.origin.x, bottom: bottom, right: 0 ))
        }
        
        closeBtn.snp.makeConstraints { (make) in
            make.bottom.centerX.equalToSuperview()
            make.width.height.equalTo(44)
        }
        
    }
    
    // MARK: - initViews
    public func initViews() {
        
        backgroundView = {
            let backgroundView = UIView()
            backgroundView.backgroundColor = UIColor.black
            return backgroundView
        }()
        let tap = UITapGestureRecognizer(target: self,
                                         action: #selector(backgroundViewTap))

        backgroundView.addGestureRecognizer(tap)
        
        closeBtn = {
            let closeBtn = UIButton()
            closeBtn.setImage(UIImage(named: "close"), for: .normal)
            return closeBtn
        }()
        
        closeBtn.isHidden = isHiddenClose
        closeBtn.addTarget(self, action: #selector(didcloseBtnTapped), for: UIControl.Event.touchUpInside)
        
    }
}

什么,你还是觉得丑,想要一个能改的默认模版?喂喂喂,过分了,兄弟,是你要求要自定义的。
既然写到了这里,我就勉为骑男的再帮你写个默认实现吧。

超进化:提供默认弹框

事先说好,我用到了snpkit,你要是没有导入,那自己计算frame吧

假装是代码
这个类有点长,放最后面吧。

这里要说的一点是提供了一个样式类,都有默认值,需要修改的,设置一下内容的style就好了。
AlertViewStyle.swift

public struct AlertViewStyle {
    public var backgroundViewAlpha: CGFloat = 0.6
    public var dialogViewCornerRadius: CGFloat = 5
    
    public var titleLFont: CGFloat = 18
    public var titleLColor = UIColor.hex("#333333")
    public var messageLFont: CGFloat = 15
    public var messageLColor = UIColor.hex("#333333")
    public var textViewFont: CGFloat = 15
    public var textViewBorderColor = UIColor.hex("#F07D8A")
    public var textViewTextCorlr = UIColor.hex("#333333")
    
    public var cancelBtnTextColor = UIColor.hex("#999999")
    public var cancelBtnTextFont: CGFloat = 16
    public var cancelBtnNormalBgColor = UIColor.hex("#EEEFF1")
//    public var cancelBtnHighlightedBgColor = UIColor.c192451
    
    public var doneBtnTextColor = UIColor.white
    public var doneBtnTextFont: CGFloat = 16
    public var doneBtnNormalBgColor = UIColor.hex("#F07D8A")
    public var doneBtnHighlightedBgColor = UIColor.hex("#F07D8A", alpha: 0.8)
    
    public var cornerRadius: CGFloat = 3
    
    public init() {}
}

效果图:


image.png

image.png

什么,你还要加入业务?!

究极进化:整合业务

调用

let al = AlertTipBusinessView.init(.giveupPay).show()
al.base.doneBtnClick = { [unowned al] in
    al.dismiss(animated: true)
}

详细代码(AlertTipBusinessView.swift):


/// 业务类型
enum TipBusinessType {
    case giveupPay
    case addAddress
    case deleteAddress
    case cancelOrder
    case remindDeliverGoods
    case secendRemind
    
    struct Data {
        var title = ""
        var subTitle = ""
        var certain = ""
        var other = ""
        init(subTitle: String, centain: String = "确认", title: String = "", other: String = "") {
            self.title = title
            self.subTitle = subTitle
            self.certain = centain
            self.other = other
        }
        
        init() {
            self.init(subTitle: "")
        }
    }
    var data: Data {
        var data = Data(subTitle: "")
        switch self {
        case .giveupPay:
            data.title = "确认要离开吗?"
            data.subTitle = "心动美物,看中了就赶紧下手哦!"
            data.certain = "继续购物"
            data.other = "残忍离开"
        case .addAddress:
            data.title = "添加地址"
            data.subTitle = "您尚未添加收件地址"
            data.certain = "添加"
            data.other = "稍后"
        case .deleteAddress:
            data.title = "删除地址"
            data.subTitle = "删除地址后不能恢复,请谨慎操作"
            data.certain = "返回"
            data.other = "删除"
        case .cancelOrder:
            data.title = "确认收货"
            data.subTitle = "请在收到货之后再确认收货"
            data.certain = "确认收货"
            data.other = "取消"
        case .remindDeliverGoods:
            data.title = "提醒发货"
            data.subTitle = "平台已经收到您的提醒,会尽快发货,请注意查收系统消息和短信"
            data.certain = "我知道了"
        case .secendRemind:
            data.subTitle = "平台已经收到您的提醒\n请勿频繁提交发货提醒"
            data.certain = "我知道了"
        }
        return data
    }
}

/// 提示业务逻辑
class AlertTipBusinessView: AlertCustomView {
    
    /// 普通用法
    /// AlertTipBusinessView.init(.secendRemind).show()
    ///
    /// 自定义按钮事件
    /// let al = AlertTipBusinessView.init(.secendRemind).show()
    /// al.base.doneBtnClick = { [unowned al] in
    ///    al.dismiss(animated: true)
    /// }
    convenience init(_ businessType: TipBusinessType) {
        let data = businessType.data
        let tipV = AlertTipView(title: data.title, message: data.subTitle, doneBtnTitle: data.certain, cancelBtnTitle: data.other)
        self.init(tipV)
        
        if data.other.isEmpty {
            tipV.doneBtnClick = { [unowned self] in
                self.dismiss()
            }
        }else {
            tipV.cancelBtnClick = { [unowned self] in
                self.dismiss()
            }
        }
    }
}

合体:其他代码

自定义UI代码(AlertTipView.swift):


import Foundation
import UIKit

public class AlertTipView: UIView {
    
    // MARK: - datas
    public var style = AlertViewStyle() {
        didSet {

            titleL.font = UIFont.boldSystemFont(ofSize: style.titleLFont)
            titleL.textColor = style.titleLColor
            
            if titleL.text?.isEmpty ?? true {
                messageL.font = UIFont.systemFont(ofSize: style.titleLFont)
            }else {
                messageL.font = UIFont.systemFont(ofSize: style.messageLFont)
            }
            messageL.textColor = style.messageLColor


            doneBtn.setTitleColor(style.doneBtnTextColor, for: .normal)
            doneBtn.titleLabel?.font = UIFont.boldSystemFont(ofSize: style.doneBtnTextFont)
            doneBtn.setBackgroundImage(UIImage(color: style.doneBtnNormalBgColor),
                                       for: .normal)
            doneBtn.setBackgroundImage(UIImage(color: style.doneBtnHighlightedBgColor),
                                       for: .highlighted)

            cancelBtn.titleLabel?.font = UIFont.boldSystemFont(ofSize: style.cancelBtnTextFont)

            cancelBtn.setTitleColor(style.cancelBtnTextColor, for: .normal)
            cancelBtn.setBackgroundImage(UIImage(color: style.cancelBtnNormalBgColor),
            for: .normal)
        }
    }
    
    public var doneBtnClick: (() -> Void)?
    public var cancelBtnClick: (() -> Void)?

    // MARK: - views
    public var whiteBgV: UIView!

    private var titleL: UILabel!
    private var messageL: UILabel!

    public var cancelBtn: UIButton! // 取消
    public var doneBtn: UIButton!

    // MARK: - leftStyle
    deinit {
        print("alertType deinit")
    }

    public convenience init(title: String, message: String, doneBtnTitle: String, cancelBtnTitle: String = "取消") {
        self.init(frame: CGRect(x: 0, y: 0, width: 265, height: 0))
        self.initialise(title: title, message: message, doneBtnTitle: doneBtnTitle, cancelBtnTitle: cancelBtnTitle)
    }

    public override init(frame: CGRect) {
        style = AlertViewStyle()
        super.init(frame: frame)
        initViews()
    }

    public required init?(coder aDecoder: NSCoder) {
        style = AlertViewStyle()
        super.init(coder: aDecoder)
        initViews()
    }

    private func initialise(title: String, message: String, doneBtnTitle: String, cancelBtnTitle: String = "取消") {

        titleL.text = title
        doneBtn.setTitle(doneBtnTitle, for: UIControl.State.normal)
        cancelBtn.setTitle(cancelBtnTitle, for: UIControl.State.normal)

        messageL.text = message
        
        cancelBtn.addTarget(self, action: #selector(didCancelBtnTapped), for: UIControl.Event.touchUpInside)
        doneBtn.addTarget(self, action: #selector(didDoneBtnTappad), for: UIControl.Event.touchUpInside)

        let hasCancel = !cancelBtnTitle.isEmpty
        hasCancel ? setupTextAlert(!title.isEmpty) : setupNoCancelTextAlert(!title.isEmpty)
        

    }

    // MARK: - events

    @objc public func didDoneBtnTappad() {
        print("doneBtn isTappad,get btn use getdoneBtn()")
        doneBtnClick?()
    }

    // dismiss the alert view
    @objc public func didCancelBtnTapped() {
        cancelBtnClick?()
    }

    // MARK: - initViews
    public func initViews() {
        whiteBgV = {
            let view = UIView()
            view.backgroundColor = UIColor.white
            view.layer.cornerRadius = style.cornerRadius
            view.clipsToBounds = true
            return view
        }()


        titleL = {
            let titleL = UILabel()
            titleL.textAlignment = .center
            titleL.lineBreakMode = NSLineBreakMode.byWordWrapping
            titleL.numberOfLines = 0
            titleL.sizeToFit()
            return titleL
        }()
         messageL = {
            let messageL = UILabel()
            messageL.numberOfLines = 0
            messageL.lineBreakMode = NSLineBreakMode.byWordWrapping
            messageL.textAlignment = .center
            messageL.sizeToFit()
            return messageL
        }()

        cancelBtn = {
            let cancelBtn = UIButton()
            cancelBtn.layer.cornerRadius = style.cornerRadius
            cancelBtn.clipsToBounds = true
            return cancelBtn
        }()

        doneBtn = {
            let doneBtn = UIButton()
            //        doneBtn.backgroundColor =  UIColor.darkText
            doneBtn.layer.cornerRadius = style.cornerRadius
            doneBtn.clipsToBounds = true
            return doneBtn
        }()
    }

    public override func touchesBegan(_ touches: Set, with event: UIEvent?) {
        print("al touchesBegan")
    }

}

// MARK: - textAlert
extension AlertTipView {

    fileprivate func setupTextAlert(_ hasTitle: Bool = true) {

        self.addSubview(whiteBgV)
        whiteBgV.addSubview(titleL)
        whiteBgV.addSubview(messageL)
        whiteBgV.addSubview(doneBtn)
        whiteBgV.addSubview(cancelBtn)

        whiteBgV.snp.makeConstraints { (make) in
            make.left.top.right.equalToSuperview()
            make.bottom.equalTo(0)
            make.width.equalTo(265)
        }

        titleL.snp.makeConstraints { (make) in
            make.top.equalTo(20)
            make.left.equalTo(15)
            make.right.equalTo(-15)
        }

        messageL.snp.makeConstraints { (make) in
            let top: CGFloat = hasTitle ? 60 : 40
            let bottom: CGFloat = hasTitle ? 70 : 84
            make.edges.equalTo(UIEdgeInsets(top: top, left: 20, bottom: bottom, right: 20))
        }

        doneBtn.snp.makeConstraints { (make) in
            make.height.equalTo(44)
            make.right.bottom.equalTo(0)
            make.width.equalToSuperview().multipliedBy(0.5)
        }

        cancelBtn.snp.makeConstraints { (make) in
            make.left.equalTo(0)
            make.bottom.width.height.equalTo(doneBtn)
        }
        doneBtn.clipsToBounds = false
        cancelBtn.clipsToBounds = false
    }


    fileprivate func setupNoCancelTextAlert(_ hasTitle: Bool = true) {
        
        self.addSubview(whiteBgV)
        whiteBgV.addSubview(titleL)
        whiteBgV.addSubview(messageL)
        whiteBgV.addSubview(doneBtn)
        whiteBgV.addSubview(cancelBtn)

        whiteBgV.snp.makeConstraints { (make) in
            make.left.top.right.equalToSuperview()
            make.bottom.equalTo(0)
            make.width.equalTo(265)
        }

        titleL.snp.makeConstraints { (make) in
            make.top.equalTo(20)
            make.left.equalTo(15)
            make.right.equalTo(-15)
        }

        messageL.snp.makeConstraints { (make) in

            let top: CGFloat = hasTitle ? 60 : 40
            let bottom: CGFloat = hasTitle ? 80 : 96
            make.edges.equalTo(UIEdgeInsets(top: top, left: 20, bottom: bottom, right: 20))
        }

        doneBtn.snp.makeConstraints { (make) in

            make.height.equalTo(36)
            make.bottom.equalTo(-20)
            make.left.equalTo(22.5)
            make.right.equalTo(-22.5)
        }
    }
}

项目中用到的小工具(AlertTools.swift)


import Foundation

//mark UIImage with downloadable content
extension UIImage {
    
    //根据颜色创建图片
    public convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) {
        let rect = CGRect(origin: .zero, size: size)
        UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
        color.setFill()
        UIRectFill(rect)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        
        guard let cgImage = image?.cgImage else { return nil }
        self.init(cgImage: cgImage)
    }
}

extension UIColor {
    
    public convenience init(hex: String) {
        self.init(hex: hex, alpha:1)
    }
    
    public convenience init(hex: String, alpha: CGFloat) {
        var hexWithoutSymbol = hex
        if hexWithoutSymbol.hasPrefix("#") {
            hexWithoutSymbol = hex.substring(from: 1)
        }
        
        let scanner = Scanner(string: hexWithoutSymbol)
        var hexInt:UInt32 = 0x0
        scanner.scanHexInt32(&hexInt)
        
        var r:UInt32!, g:UInt32!, b:UInt32!
        switch (hexWithoutSymbol.count) {
        case 3: // #RGB
            r = ((hexInt >> 4) & 0xf0 | (hexInt >> 8) & 0x0f)
            g = ((hexInt >> 0) & 0xf0 | (hexInt >> 4) & 0x0f)
            b = ((hexInt << 4) & 0xf0 | hexInt & 0x0f)
            break;
        case 6: // #RRGGBB
            r = (hexInt >> 16) & 0xff
            g = (hexInt >> 8) & 0xff
            b = hexInt & 0xff
            break;
        default:
            
            print("UIColor init error: hex == \(hex), alpha == \(alpha)")
            break;
        }
        
        self.init(
            red: (CGFloat(r)/255),
            green: (CGFloat(g)/255),
            blue: (CGFloat(b)/255),
            alpha:alpha)
    }
    
    public static func hex(_ hex: String) -> UIColor{
         return UIColor(hex: hex)
    }
    
    public static func hex(_ hex: String, alpha: CGFloat) -> UIColor{
        return UIColor(hex: hex, alpha: alpha)
    }

}

// MARK: - substring
extension String {
    /// 返回Index类型
    ///
    public func index(from: Int) -> Index {
        return self.index(startIndex, offsetBy: from)
    }
    /// 裁剪字符串from
    ///
    /// - Parameter from: 从哪里开始
    public func substring(from: Int) -> String {
        let fromIndex = index(from: from)

        return String(suffix(from: fromIndex))
    }

    /// 裁剪字符串to
    ///
    /// - Parameter to: 到哪里结束
    public func substring(to: Int) -> String {
        let toIndex = index(from: to)
        return String(prefix(upTo: toIndex))
    }
}

你可能感兴趣的:(如何设计一个优雅的弹框,Swift版)