用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
}
}
