请稍侯

用swift 封装一个 mess

22 August 2023

用swift 封装一个 MessageToast 工具类

@objc public enum MessageToastDirection: Int {
    case bottomInBottomOut
    case leftInLeftOut
}

@objc public enum MessageToastPriority: Int {
    case low = 0
    case normal = 1
    case high = 2
}

@objc public protocol MessageToastable: NSObjectProtocol {
    var priority: MessageToastPriority { get set }
    func viewSize() -> CGSize
}

typealias ToastViewType = UIView & MessageToastable


@objc
public class MessageToast: NSObject {

    private static let shared = MessageToast()
    private override init(){
        super.init()
    }

    static func instance() -> MessageToast {
        MessageToast()
    }

    //MARK: show Toast Message

    //typealias MessageItem<T: UIView & MessageToastable> = (toastView: T, duration: Double)
    //typealias MessageItem<T> = (toastView: T, duration: Double) where T: UIView, T: MessageToastable
    typealias MessageItem = (getPreparedToastView: (Any?) -> ToastViewType?, toastVM: Any?, position: CGPoint,
                             priority: MessageToastPriority, direction: MessageToastDirection, duration: Double)

    private var toastQueue: [MessageItem] = []
    private var isShowingToast: Bool = false

    ///position: position is start point, such as leftCenter for leftInLeftOut, bottomCenter for bottomInBottomOut
    func show(getPreparedToastView: @escaping (Any?) -> ToastViewType?, toastVM: Any?,  position: CGPoint,
              priority: MessageToastPriority = .normal,
              direction: MessageToastDirection = .bottomInBottomOut,
              duration: Double = 2.0) {

        let data: MessageItem = (getPreparedToastView: getPreparedToastView, toastVM: toastVM, position: position,
                priority: priority, direction: direction, duration: duration)

        self.toastQueue.append(data)

        //clip queue
        let limit = 2
        if self.toastQueue.count >= limit {
            let oldQueue = self.toastQueue
            let startIndex = oldQueue.count - limit
            let endIndex = oldQueue.count
            self.toastQueue = Array(oldQueue[startIndex..<endIndex])
        }

        if !self.isShowingToast {
            self.showNextToast()
        }
    }

    private var firstItem: (item: MessageItem, index: Int)? {
        let index = (self.toastQueue.firstIndex(where: { $0.priority == .high }) ??
                self.toastQueue.firstIndex(where: { $0.priority == .normal }) ??
                self.toastQueue.startIndex )
        
        guard let item = self.toastQueue[safe: index] else { return nil }
        
        return (item, index)
    }

    private func showNextToast() {
        
        guard let data = self.firstItem?.item, let index = self.firstItem?.index else { return }
        if self.toastQueue.count >  index {
            //remove the item from self.toastQueue
            self.toastQueue.remove(at: index)
        }

        //guard let parentView = NavigationPushManager.topViewController()?.view else { return }

        self.isShowingToast = true

        guard let toastView = data.getPreparedToastView(data.toastVM) as ToastViewType?,  toastView.superview != nil else {
            self.showNextToast()
            return
        }

        // makeConstraints
        switch data.direction {
        case .bottomInBottomOut:
            toastView.snp.remakeConstraints { make in
                make.size.equalTo(toastView.viewSize())
                make.centerX.equalTo(data.position.x)
                make.top.equalToSuperview().offset(data.position.y)
            }

        case .leftInLeftOut:
            toastView.snp.remakeConstraints { make in
                let toastViewSize = toastView.viewSize()
                make.size.equalTo(toastViewSize)
                make.centerY.equalTo(data.position.y)
                make.left.equalToSuperview().offset(data.position.x - toastViewSize.width)
            }
        }
        toastView.alpha = 1.0

        toastView.superview?.layoutIfNeeded()
        toastView.layoutIfNeeded()

        //show
        DispatchQueue.main.async { [weak self, weak toastView] in
            guard let self = self, let toastView = toastView else { return }

            self.showToastView(toastView, data: data) {  [weak self] in
                guard let self = self else { return }
                self.isShowingToast = false
                self.showNextToast()
            }
        }

    }

    private func showToastView(_ toastView: ToastViewType, data: MessageItem, completion: (()->Void)? = nil) {
        guard toastView.superview != nil else { return }
        
        switch data.direction {
        case .bottomInBottomOut:
            toastView.snp.updateConstraints { make in
                let toastViewSize = toastView.viewSize()
                make.top.equalToSuperview().offset(data.position.y + toastViewSize.height)
            }
        case .leftInLeftOut:
            toastView.snp.updateConstraints { make in
                make.left.equalToSuperview().offset(data.position.x)
            }
        }

        UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseIn, animations: {
            toastView.superview?.layoutIfNeeded()

        }, completion: { [weak self] _ in
            guard let self = self else { return }
            DispatchQueue.main.asyncAfter(deadline: .now() + data.duration) {
                self.hideToastView(toastView, data: data, completion: completion)
            }
        })

        UIView.animate(withDuration: 0.1, delay: 0.0, options: .curveEaseIn) {
            toastView.alpha = 1.0
        }
    }

    private func hideToastView(_ toastView: ToastViewType, data: MessageItem, completion: (()->Void)? = nil) {
        guard toastView.superview != nil else { return }
        
        switch data.direction {
        case .bottomInBottomOut:
            toastView.snp.updateConstraints { make in
                make.top.equalToSuperview().offset(data.position.y)
            }
        case .leftInLeftOut:
            toastView.snp.updateConstraints { make in
                let toastViewSize = toastView.viewSize()
                make.left.equalToSuperview().offset(data.position.x - toastViewSize.width)
            }
        }

        DispatchQueue.main.async { [weak toastView] in
            guard let toastView = toastView else { return }

            UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveLinear, animations: {
                toastView.superview?.layoutIfNeeded()

            }, completion: { _ in
                toastView.removeFromSuperview()
                completion?()
            })

            LUIAnimator.animate(duration: 0.15, delay: 0.3, timingFunction: .cubic(animationCurve: .easeInOut)) {
                toastView.alpha = 0
            } completion: { _ in
                //do nothing
            }

        }

    }


    //MARK: show message box

    typealias MessageBoxItem = (message: String?, detail: String?, view: UIView?, leftIconImageUrlString: String?,
                                duration: Double, animated: Bool, rightButtonAction: (() -> Void)?)

    private var messageBoxQueue: [MessageBoxItem] = []
    private var isShowingMessageBox: Bool = false

    @objc public static func showMessageBox(_ message: String?,
                                      detail: String? = nil,
                                      inView view: UIView? = nil,
                                      leftIconImageUrlString: String? = nil,
                                      duration: Double = 5.0,
                                      animated: Bool = true,
                                      rightButtonAction: (() -> Void)? = nil) {
        let data: MessageBoxItem = (message: message, detail: detail, view: view, leftIconImageUrlString: leftIconImageUrlString,
                duration: duration, animated: animated, rightButtonAction: rightButtonAction)
        //clip queue
        let limit = 10
        if MessageToast.shared.messageBoxQueue.count >= limit {
            let oldQueue = MessageToast.shared.messageBoxQueue
            let startIndex = oldQueue.count - limit
            let endIndex = oldQueue.count
            MessageToast.shared.messageBoxQueue = Array(oldQueue[startIndex..<endIndex])
        }

        //append data
        MessageToast.shared.messageBoxQueue.append(data)

        //show
        if !MessageToast.shared.isShowingMessageBox {
            MessageToast.shared.showNextMessageBox()
        }
    }

    private func showNextMessageBox() {

        guard let data = messageBoxQueue.first as MessageBoxItem? else { return }
        
        guard let topView = data.view ?? NavigationPushManager.topViewController()?.view else { return }
        
        self.isShowingMessageBox = true

        let messageBox = self.createLUIMessageBox(imageUrl: .url(string: data.leftIconImageUrlString), rightButtonAction: data.rightButtonAction)
        messageBox.title = data.message ?? ""
        messageBox.text = data.detail ?? ""
        if messageBox.superview == nil {
            topView.addSubviews(messageBox)
            let inset = messageBox.viewSize().height
            messageBox.snp.makeConstraints { (make) in
                make.centerX.equalToSuperview()
                make.bottom.equalToSuperview().inset(-(inset < 48.0 ? 48.0 : inset))
            }
        }
        messageBox.superview?.layoutIfNeeded()

        //show
        DispatchQueue.main.async { [weak self, weak messageBox] in
            guard let self = self, let messageBox = messageBox else { return }
            self.showMessageBox(messageBox, data: data)
        }

    }


    private func showMessageBox(_ messageBox: LUIMessageBoxV2, data: MessageBoxItem, completion: (() -> Void)? = nil ) {

        messageBox.snp.updateConstraints { (make) in
            make.bottom.equalToSuperview().inset(34.pt)
        }

        LUIAnimator.animate(duration: data.animated ? 0.35 : 0, delay: 0, timingFunction: .spring(mass: 0.5, stiffness: 100, damping: 10, initialVelocity: .init(dx: 15, dy: 15))) {
            messageBox.superview?.layoutIfNeeded()
        } completion: { (_) in
            /* completion?() */
        }

        LUIAnimator.animate(duration: data.animated ? 0.07 : 0, delay: 0, timingFunction: .cubic(animationCurve: .easeInOut)) {
            messageBox.alpha = 1
        }

        //hide
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5 + data.duration) { [weak self, weak messageBox] in
            guard let self = self, let messageBox = messageBox else { return }
            self.hideMessageBox(messageBox, data: data)
        }
    }

    private func hideMessageBox(_ messageBox: LUIMessageBoxV2, data: MessageBoxItem, completion: (() -> Void)? = nil ) {
        messageBox.snp.updateConstraints { (make) in
            make.bottom.equalToSuperview().inset(-messageBox.viewSize().height)
        }

        LUIAnimator.animate(duration: data.animated ? 0.25 : 0, delay: 0, timingFunction: .cubic(animationCurve: .easeIn)) {
            messageBox.superview?.layoutIfNeeded()
            
        } completion: { [weak self] (_) in
            completion?()
            messageBox.removeFromSuperview()

            guard let self = self else { return }
            self.isShowingMessageBox = false
            if self.messageBoxQueue.count > 0 {
                self.messageBoxQueue.removeFirst()
                self.showNextMessageBox()
            }

        }

        LUIAnimator.animate(duration: 0.14, delay: 0.1, timingFunction: .cubic(animationCurve: .easeInOut)) {
            messageBox.alpha = 0
        } completion: { _ in
            //do nothing
        }
    }

    private func createLUIMessageBox(imageUrl: URL?, rightButtonAction: (() -> Void)? = nil) -> LUIMessageBoxV2 {

        var leftIconType = LUIMessageBoxV2.Size.LeftIconType.Medium.icon
        var leftIcon: LUIImageType = LUIImageType.icon(.checkCircleFill)
        if let imageUrl = imageUrl {
            leftIconType = LUIMessageBoxV2.Size.LeftIconType.Medium.imageSquare
            leftIcon = LUIImageType.url(imageUrl, size: CGSize(width: 30, height: 30))
        }

        var rightButtonType = LUIMessageBoxV2.Size.RightButtonType.Medium.none
        if rightButtonAction != nil {
            rightButtonType = LUIMessageBoxV2.Size.RightButtonType.Medium.textNoArrow
        }

        let msgBoxView = LUIMessageBoxV2(size: .medium(leftIconType: leftIconType, rightButtonType: rightButtonType), type: .tint, color: .bluegray,
                usableWidth: UIScreen.main.bounds.width - 8.pt * 2, defaultTextColor: .white)

        msgBoxView.backgroundColor = LUI.bluegray900
        msgBoxView.titleColor = .white
        msgBoxView.leftIcon = leftIcon

        if let imageUrl = imageUrl {
            msgBoxView.customLeftIconSize = true
        }else{
            msgBoxView.leftIconColor = LUI.green500
        }

        if rightButtonAction != nil {
            msgBoxView.rightButtonText = "바로가기".localized
            msgBoxView.onClickRightButton = rightButtonAction
        }

        return msgBoxView
    }


}