diff --git a/PHDemo/PHDemo/AlertVC.swift b/PHDemo/PHDemo/AlertVC.swift index d33ba3d..54fa2a3 100644 --- a/PHDemo/PHDemo/AlertVC.swift +++ b/PHDemo/PHDemo/AlertVC.swift @@ -29,14 +29,15 @@ class AlertVC: ListVC { list.add(title: "纯文字") { section in section.add(title: "只有一句话") { - Alert(.message("只有一句话").duration(2)) + // Alert(.message("只有一句话").duration(2)) + // 可以简写成这样: + Alert("只有一句话") } section.add(title: "标题 + 正文") { let title = "这是标题" let message = "这是正文,文字支持自动换行,可设置最小宽度和最大宽度。这个弹窗将会持续4秒。" Alert { alert in - alert.vm = .title(title).message(message) - alert.vm.duration = 4 + alert.vm = .title(title).message(message).duration(4) } } } @@ -62,9 +63,9 @@ class AlertVC: ListVC { } section.add(title: "图标 + 标题 + 正文") { Alert(.error) { alert in - alert.vm.title = "加载失败" - alert.vm.message = "请稍后重试" - alert.vm.duration = 3 + alert.vm?.title = "加载失败" + alert.vm?.message = "请稍后重试" + alert.vm?.duration = 3 } } } @@ -81,7 +82,7 @@ class AlertVC: ListVC { alert.config.customButton { button in button.titleLabel?.font = .systemFont(ofSize: 15) } - alert.vm.title = "你正在使用移动网络观看" + alert.title = "你正在使用移动网络观看" alert.onViewDidLoad { vc in guard let alert = vc as? AlertTarget else { return @@ -111,7 +112,7 @@ class AlertVC: ListVC { alert.config.customButton { button in button.titleLabel?.font = .systemFont(ofSize: 15) } - alert.vm.message = "为了维护社区氛围,上麦用户需进行主播认证" + alert.vm?.message = "为了维护社区氛围,上麦用户需进行主播认证" alert.onViewDidLoad { vc in guard let alert = vc as? AlertTarget else { return @@ -141,7 +142,7 @@ class AlertVC: ListVC { alert.config.customButton { button in button.titleLabel?.font = .systemFont(ofSize: 15) } - alert.vm.message = "本次消费需要你支付999软妹豆,确认支付吗?" + alert.vm?.message = "本次消费需要你支付999软妹豆,确认支付吗?" alert.config.customActionStack { stack in stack.spacing = 0 stack.axis = .vertical // 竖排按钮 @@ -187,15 +188,15 @@ class AlertVC: ListVC { section.add(title: "只有一段文字 + 按钮") { Alert { alert in - alert.vm.title = "只有一段文字" + alert.title = "只有一段文字" alert.add(action: "取消", style: .gray) alert.add(action: "默认按钮") } } section.add(title: "标题 + 正文 + 按钮") { Alert { alert in - alert.vm.title = "标题" - alert.vm.message = "这是一段正文,长度超出最大宽度时会自动换行" + alert.vm?.title = "标题" + alert.vm?.message = "这是一段正文,长度超出最大宽度时会自动换行" alert.add(action: "取消", style: .gray) alert.add(action: "删除", style: .destructive) { alert in // 自定义了按钮事件之后,需要手动pop弹窗 @@ -229,8 +230,8 @@ class AlertVC: ListVC { } section.add(title: "确认删除") { Alert(.delete) { alert in - alert.vm.title = "确认删除" - alert.vm.message = "此操作无法撤销" + alert.vm?.title = "确认删除" + alert.vm?.message = "此操作无法撤销" alert.add(action: "取消", style: .gray) alert.add(action: "删除", style: .destructive) } @@ -239,7 +240,7 @@ class AlertVC: ListVC { list.add(title: "控件管理") { section in section.add(title: "按钮增删改查") { Alert(.note) { alert in - alert.vm.message = "可以动态增加、删除按钮" + alert.vm?.message = "可以动态增加、删除按钮" alert.add(action: "在底部增加按钮", style: .filled(color: .systemGreen)) { alert in alert.add(action: "哈哈1", identifier: "haha1") } @@ -265,36 +266,34 @@ class AlertVC: ListVC { } } section.add(title: "更新文字") { - Alert(.note) { alert in - alert.vm.message = "可以动态增加、删除、更新文字" + Alert(.note.message("可以动态增加、删除、更新文字")) { alert in alert.add(action: "增加标题") { alert in - alert.vm.title = "这是标题" + alert.vm?.title = "这是标题" alert.reloadTextStack() } alert.add(action: "增加正文") { alert in - alert.vm.message = "可以动态增加、删除、更新文字" + alert.vm?.message = "可以动态增加、删除、更新文字" alert.reloadTextStack() } alert.add(action: "删除标题", style: .destructive) { alert in - alert.vm.title = nil + alert.vm?.title = nil alert.reloadTextStack() } alert.add(action: "删除正文", style: .destructive) { alert in - alert.vm.message = nil + alert.vm?.message = nil alert.reloadTextStack() } alert.add(action: "取消", style: .gray) } } section.add(title: "在弹出过程中增加元素") { - Alert(.loading) { alert in - alert.vm.title = "在弹出过程中增加元素" + Alert(.loading.title("在弹出过程中增加元素")) { alert in alert.add(action: "OK", style: .gray) alert.onViewWillAppear { vc in guard let alert = vc as? AlertTarget else { return } - alert.vm.message = "这是一段后增加的文字\n动画效果会有细微差别" + alert.vm?.message = "这是一段后增加的文字\n动画效果会有细微差别" alert.reloadTextStack() } } @@ -304,8 +303,8 @@ class AlertVC: ListVC { section.add(title: "多层级弹窗") { func f(i: Int) { Alert { alert in - alert.vm.title = "第\(i)次弹" - alert.vm.message = "每次都是一个新的实例覆盖在上一个弹窗上面,而背景不会叠加变深。" + alert.vm?.title = "第\(i)次弹" + alert.vm?.message = "每次都是一个新的实例覆盖在上一个弹窗上面,而背景不会叠加变深。" alert.add(action: "取消", style: .gray) alert.add(action: "增加一个") { alert in f(i: i + 1) @@ -357,7 +356,7 @@ class AlertVC: ListVC { list.add(title: "自定义视图") { section in section.add(title: "自定义控件") { Alert { alert in - alert.vm.title = "自定义控件" + alert.title = "自定义控件" // 图片 let imgv = UIImageView(image: UIImage(named: "landscape")) imgv.contentMode = .scaleAspectFill diff --git a/PHDemo/PHDemo/CapsuleVC.swift b/PHDemo/PHDemo/CapsuleVC.swift index 335d72f..271cb20 100644 --- a/PHDemo/PHDemo/CapsuleVC.swift +++ b/PHDemo/PHDemo/CapsuleVC.swift @@ -17,12 +17,15 @@ class CapsuleVC: ListVC { header.detailLabel.text = "状态胶囊控件,用于状态显示,一个主程序窗口每个位置(上中下)各自最多只有一个状态胶囊实例。" CapsuleConfiguration.global { config in + config.defaultDuration = 3 // 默认的持续时间 // config.cardCornerRadius = .infinity // 设置一个较大的数字就会变成胶囊形状 } list.add(title: "默认布局:纯文字") { section in section.add(title: "一条简短的消息") { // 设置vm或者handler都会自动push,这里测试传入vm: - Capsule(.message("一条简短消息")) + // Capsule(.message("一条简短消息")) + // 如果只有一条文字信息,可以直接传字符串: + Capsule("一条简短消息") } section.add(title: "一条稍微长一点的消息") { // 设置vm或者handler都会自动push,这里测试传入handler: @@ -31,12 +34,14 @@ class CapsuleVC: ListVC { capsule.vm = .message("一条稍微长一点的消息") } } - section.add(title: "(默认)状态胶囊控件,用于状态显示,一个主程序窗口只有一个状态胶囊实例。") { + section.add(title: "延迟显示") { // 也可以创建一个空白实例,在需要的时候再push let obj = Capsule().target obj.vm = .message("状态胶囊控件,用于状态显示,一个主程序窗口只有一个状态胶囊实例。") // ... 在需要的时候手动push - obj.push() + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + obj.push() + } } section.add(title: "(限制1行)状态胶囊控件,用于状态显示,一个主程序窗口只有一个状态胶囊实例。") { // 同时设置vm和handler也可以 @@ -146,7 +151,7 @@ class CapsuleVC: ListVC { } -extension CapsuleTarget.ViewModel { +extension CapsuleViewModel { static func info(_ text: String?) -> Self { .init() diff --git a/PHDemo/PHDemo/SheetVC.swift b/PHDemo/PHDemo/SheetVC.swift index e368553..ec62d7e 100644 --- a/PHDemo/PHDemo/SheetVC.swift +++ b/PHDemo/PHDemo/SheetVC.swift @@ -25,7 +25,7 @@ class SheetVC: ListVC { sheet.add(spacing: 24) sheet.add(action: "确认", style: .destructive) { sheet in Alert(.confirm) { alert in - alert.vm.title = "处理点击事件" + alert.title = "处理点击事件" alert.add(action: "我知道了") } } @@ -74,12 +74,11 @@ class SheetVC: ListVC { sheet.add(action: "确认") sheet.add(action: "取消", style: .gray) sheet.onTappedBackground { sheet in - print("点击了背景") Toast.lazyPush(identifier: "alert") { toast in toast.vm = .error - toast.vm.title = "点击了背景" - toast.vm.message = "点击背景将不会dismiss,必须在下方做出选择才能关掉" - toast.vm.duration = 2 + .title("点击了背景") + .message("点击背景将不会dismiss,必须在下方做出选择才能关掉") + .duration(2) } } } diff --git a/PHDemo/PHDemo/ToastVC.swift b/PHDemo/PHDemo/ToastVC.swift index 6c51fab..1a515c3 100644 --- a/PHDemo/PHDemo/ToastVC.swift +++ b/PHDemo/PHDemo/ToastVC.swift @@ -39,7 +39,10 @@ class TestToastTarget: ToastTarget { } } -typealias TestToast = HUDProvider +//typealias TestToast = HUDProvider +class TestToast: ToastProvider { + typealias Target = TestToastTarget +} class ToastVC: ListVC { @@ -54,6 +57,7 @@ class ToastVC: ListVC { header.detailLabel.text = message ToastConfiguration.global { config in + config.defaultDuration = 5 config.contentViewMask { mask in mask.backgroundColor = .clear mask.effect = UIBlurEffect(style: .systemChromeMaterial) @@ -68,7 +72,9 @@ class ToastVC: ListVC { TestToast(.title(title).message(message)) } section.add(title: "一段长文本") { - Toast(.message(message)) + // Toast(.message(message)) + // 可以简写成这样: + Toast(message) } section.add(title: "图标 + 标题 + 正文") { let s1 = "笑容正在加载" @@ -80,8 +86,10 @@ class ToastVC: ListVC { toast.update(progress: percent) } completion: { toast.update { toast in - toast.vm = .success(5).title("加载成功").message("这条通知5s后消失") - toast.vm.icon = UIImage(named: "twemoji") + toast.vm = .success(5) + .title("加载成功") + .message("这条通知5s后消失") + .icon(.init(named: "twemoji")) } } } @@ -119,16 +127,15 @@ class ToastVC: ListVC { section.add(title: "增加按钮") { let title = "您收到了一条好友申请" let message = "丹妮莉丝·坦格利安申请添加您为好友,是否同意?" - Toast(.title(title).message(message)) { toast in + Toast(.title(title).message(message).icon(.init(named: "avatar"))) { toast in toast.isRemovable = false - toast.vm.icon = UIImage(named: "avatar") toast.imageView.layer.masksToBounds = true toast.imageView.layer.cornerRadius = toast.config.iconSize.width / 2 toast.add(action: "拒绝", style: .destructive) { toast in Alert.lazyPush(identifier: "Dracarys") { alert in alert.vm = .message("Dracarys") - alert.vm.icon = UIImage(inProHUD: "prohud.windmill") - alert.vm.rotation = .init(repeatCount: .infinity) + .icon(UIImage(inProHUD: "prohud.windmill")) + .rotation(.init(repeatCount: .infinity)) alert.config.enableShadow = false alert.config.contentViewMask { mask in mask.effect = .none @@ -217,7 +224,7 @@ class ToastVC: ListVC { } section.add(title: "修改左右外边距") { Toast(.message("这条toast的左右外边距经过自定义设置,与其它的有所不同。")) { toast in - toast.config.windowEdgeInset = 8 + toast.config.marginX = 32 toast.config.cardCornerRadius = 24 } } @@ -306,7 +313,7 @@ class ToastVC: ListVC { section.add(title: "卡片背景样式") { Toast { toast in - toast.vm.title = "卡片背景样式" + toast.title = "卡片背景样式" toast.add(action: "浅色毛玻璃") { toast in toast.contentMaskView.effect = UIBlurEffect(style: .light) toast.contentMaskView.backgroundColor = .clear @@ -327,7 +334,7 @@ class ToastVC: ListVC { func foo() { Toast { toast in toast.title = "共享配置" - toast.vm.message = "建议在App启动后进行通用配置设置,所有实例都会先拉取通用配置为默认值,修改这些配置会影响到所有实例。" + toast.vm?.message = "建议在App启动后进行通用配置设置,所有实例都会先拉取通用配置为默认值,修改这些配置会影响到所有实例。" toast.add(action: "默认", style: .gray) { toast in ToastConfiguration.global { config in config.customTitleLabel { titleLabel in @@ -359,7 +366,7 @@ class ToastVC: ListVC { fileprivate func testAlert() { Alert { alert in - alert.vm.title = "处理点击事件" + alert.title = "处理点击事件" alert.add(action: "我知道了", style: .destructive) } } diff --git a/Sources/ProHUD/Alert/AlertDefaultLayout.swift b/Sources/ProHUD/Alert/AlertDefaultLayout.swift index 825e0f6..d0c3c2a 100644 --- a/Sources/ProHUD/Alert/AlertDefaultLayout.swift +++ b/Sources/ProHUD/Alert/AlertDefaultLayout.swift @@ -17,7 +17,7 @@ extension AlertTarget: DefaultLayout { if self.cfg.customReloadData?(self) == true { return } - view.tintColor = vm.tintColor ?? config.tintColor + view.tintColor = vm?.tintColor ?? config.tintColor let isFirstLayout: Bool if contentView.superview == nil { isFirstLayout = animated @@ -126,7 +126,7 @@ extension AlertTarget: DefaultLayout { func updateTimeoutDuration() { // 设置持续时间 - vm.timeoutHandler = DispatchWorkItem(block: { [weak self] in + vm?.timeoutHandler = DispatchWorkItem(block: { [weak self] in self?.pop() }) } @@ -144,9 +144,9 @@ extension AlertTarget { // 移除进度 progressView?.removeFromSuperview() - if vm.icon != nil || vm.iconURL != nil { - imageView.image = vm.icon - if let iconURL = vm.iconURL { + if vm?.icon != nil || vm?.iconURL != nil { + imageView.image = vm?.icon + if let iconURL = vm?.iconURL { config.customWebImage?(imageView, iconURL) } if imageView.superview == nil { @@ -159,7 +159,7 @@ extension AlertTarget { mk.height.equalTo(config.iconSize.height) } } - if let rotation = vm.rotation { + if let rotation = vm?.rotation { startRotate(rotation) } } else { @@ -171,8 +171,8 @@ extension AlertTarget { } func setupTextStack() { - let titleCount = vm.title?.count ?? 0 - let bodyCount = vm.message?.count ?? 0 + let titleCount = vm?.title?.count ?? 0 + let bodyCount = vm?.message?.count ?? 0 if titleCount > 0 || bodyCount > 0 { if textStack.superview != contentStack { if let index = contentStack.arrangedSubviews.firstIndex(of: imageView) { @@ -189,7 +189,7 @@ extension AlertTarget { } } if titleCount > 0 { - titleLabel.text = vm.title + titleLabel.text = vm?.title if titleLabel.superview != textStack { textStack.insertArrangedSubview(titleLabel, at: 0) } @@ -209,7 +209,7 @@ extension AlertTarget { titleLabel.removeFromSuperview() } if bodyCount > 0 { - bodyLabel.text = vm.message + bodyLabel.text = vm?.message if bodyLabel.superview != textStack { textStack.addArrangedSubview(bodyLabel) } diff --git a/Sources/ProHUD/Alert/AlertProvider.swift b/Sources/ProHUD/Alert/AlertProvider.swift index f97d71b..15c18c6 100644 --- a/Sources/ProHUD/Alert/AlertProvider.swift +++ b/Sources/ProHUD/Alert/AlertProvider.swift @@ -8,13 +8,30 @@ import UIKit open class AlertProvider: HUDProvider { - @discardableResult public required init(_ vm: ViewModel?, initializer: ((_ alert: Target) -> Void)?) { - super.init(vm, initializer: initializer) + + public typealias ViewModel = AlertViewModel + public typealias Target = AlertTarget + + @discardableResult @objc public required init(initializer: ((_ alert: Target) -> Void)?) { + super.init(initializer: initializer) } - @discardableResult public required convenience init(initializer: ((_ alert: Target) -> Void)?) { - self.init(nil, initializer: initializer) + @discardableResult public convenience init(_ vm: ViewModel, initializer: ((_ alert: Target) -> Void)?) { + self.init { alert in + alert.vm = vm + initializer?(alert) + } } + /// 根据ViewModel创建一个Target并显示 + /// - Parameter vm: 数据模型 + @discardableResult public convenience init(_ vm: ViewModel) { + self.init(vm, initializer: nil) + } + + @discardableResult @objc public convenience init(_ text: String, duration: TimeInterval = 3) { + self.init(.message(text).duration(duration), initializer: nil) + } + /// 如果不存在就创建并弹出一个HUD实例,如果存在就更新实例 /// - Parameters: diff --git a/Sources/ProHUD/Alert/AlertTarget.swift b/Sources/ProHUD/Alert/AlertTarget.swift index 1a520f5..57709ba 100644 --- a/Sources/ProHUD/Alert/AlertTarget.swift +++ b/Sources/ProHUD/Alert/AlertTarget.swift @@ -74,11 +74,15 @@ open class AlertTarget: BaseController, HUDTargetType { }() /// 视图模型 - @objc public var vm: AlertViewModel = .init() + @objc public var vm: AlertViewModel? public override var title: String? { didSet { - vm.title = title + if let vm = vm { + vm.title = title + } else { + vm = .title(title) + } } } diff --git a/Sources/ProHUD/Capsule/CapsuleConfiguration.swift b/Sources/ProHUD/Capsule/CapsuleConfiguration.swift index 35e03b6..608e3a7 100644 --- a/Sources/ProHUD/Capsule/CapsuleConfiguration.swift +++ b/Sources/ProHUD/Capsule/CapsuleConfiguration.swift @@ -19,6 +19,9 @@ public class CapsuleConfiguration: CommonConfiguration { customGlobalConfig = callback } + /// 默认的持续时间 + public var defaultDuration: TimeInterval = 3 + override var cardCornerRadiusByDefault: CGFloat { cardCornerRadius ?? 16 } diff --git a/Sources/ProHUD/Capsule/CapsuleDefaultLayout.swift b/Sources/ProHUD/Capsule/CapsuleDefaultLayout.swift index fff99ef..05aa75b 100644 --- a/Sources/ProHUD/Capsule/CapsuleDefaultLayout.swift +++ b/Sources/ProHUD/Capsule/CapsuleDefaultLayout.swift @@ -18,7 +18,7 @@ extension CapsuleTarget: DefaultLayout { return } - view.tintColor = vm.tintColor ?? config.tintColor + view.tintColor = vm?.tintColor ?? config.tintColor // content loadContentViewIfNeeded() @@ -29,7 +29,7 @@ extension CapsuleTarget: DefaultLayout { // text textLabel.removeFromSuperview() - var text = [vm.title ?? "", vm.message ?? ""].filter({ $0.count > 0 }).joined(separator: " ") + var text = [vm?.title ?? "", vm?.message ?? ""].filter({ $0.count > 0 }).joined(separator: " ") if text.count > 0 { contentStack.addArrangedSubview(textLabel) textLabel.snp.makeConstraints { make in @@ -81,11 +81,11 @@ extension CapsuleTarget: DefaultLayout { private func updateTimeoutDuration() { // 为空时使用默认规则 - if vm.duration == nil { - vm.duration = 3 + if vm?.duration == nil { + vm?.duration = config.defaultDuration } // 设置持续时间 - vm.timeoutHandler = DispatchWorkItem(block: { [weak self] in + vm?.timeoutHandler = DispatchWorkItem(block: { [weak self] in self?.pop() }) } @@ -99,16 +99,16 @@ extension CapsuleTarget: DefaultLayout { // 移除进度 progressView?.removeFromSuperview() - if vm.icon == nil && vm.iconURL == nil { + if vm?.icon == nil && vm?.iconURL == nil { contentStack.removeArrangedSubview(imageView) } else { contentStack.insertArrangedSubview(imageView, at: 0) } - imageView.image = vm.icon - if let iconURL = vm.iconURL { + imageView.image = vm?.icon + if let iconURL = vm?.iconURL { config.customWebImage?(imageView, iconURL) } - if let rotation = vm.rotation { + if let rotation = vm?.rotation { startRotate(rotation) } diff --git a/Sources/ProHUD/Capsule/CapsuleManager.swift b/Sources/ProHUD/Capsule/CapsuleManager.swift index 8261eda..55e9a1f 100644 --- a/Sources/ProHUD/Capsule/CapsuleManager.swift +++ b/Sources/ProHUD/Capsule/CapsuleManager.swift @@ -13,7 +13,7 @@ extension CapsuleTarget { guard CapsuleConfiguration.isEnabled else { return } let isNew: Bool let window: CapsuleWindow - let position = vm.position + let position = vm?.position ?? .top if let w = AppContext.current?.capsuleWindows[position] { isNew = false @@ -30,10 +30,10 @@ extension CapsuleTarget { // 应用到frame let newFrame: CGRect - switch vm.position { - case .top: - let topLayoutMargins = AppContext.appWindow?.layoutMargins.top ?? 8 - let y = max(topLayoutMargins - 8, 8) + switch vm?.position { + case .top, .none: + let topLayoutMargins = AppContext.appWindow?.safeAreaInsets.top ?? 8 + let y = max(topLayoutMargins, 8) newFrame = .init(x: (AppContext.appBounds.width - size.width) / 2, y: y, width: size.width, height: size.height) case .middle: newFrame = .init(x: (AppContext.appBounds.width - size.width) / 2, y: (AppContext.appBounds.height - size.height) / 2 - 20, width: size.width, height: size.height) @@ -60,6 +60,10 @@ extension CapsuleTarget { AppContext.capsuleWindows[s]?[position] = window } navEvents[.onViewWillAppear]?(self) + + // 更新toast防止重叠 + ToastWindow.updateToastWindowsLayout() + if isNew { window.isHidden = false func completion() { @@ -116,8 +120,11 @@ extension CapsuleTarget { @objc open func pop() { guard let window = attachedWindow, let windowScene = windowScene else { return } - AppContext.capsuleWindows[windowScene]?[vm.position] = nil + AppContext.capsuleWindows[windowScene]?[vm?.position ?? .top] = nil navEvents[.onViewWillDisappear]?(self) + // 更新toast防止重叠 + ToastWindow.updateToastWindowsLayout() + func completion() { window.isHidden = true window.transform = .identity @@ -128,8 +135,8 @@ extension CapsuleTarget { } else { let duration = config.animateDurationForBuildOutByDefault let oldFrame = window.frame - switch vm.position { - case .top: + switch vm?.position { + case .top, .none: UIView.animateEaseOut(duration: duration) { window.transform = .init(translationX: 0, y: -oldFrame.maxY - 20) } completion: { done in diff --git a/Sources/ProHUD/Capsule/CapsuleProvider.swift b/Sources/ProHUD/Capsule/CapsuleProvider.swift index 1a0bd9a..fa8e969 100644 --- a/Sources/ProHUD/Capsule/CapsuleProvider.swift +++ b/Sources/ProHUD/Capsule/CapsuleProvider.swift @@ -7,20 +7,37 @@ import UIKit -open class CapsuleProvider: HUDProvider { +open class CapsuleProvider: HUDProvider { + + public typealias ViewModel = CapsuleViewModel + public typealias Target = CapsuleTarget + + @discardableResult @objc public required init(initializer: ((_ capsule: Target) -> Void)?) { + super.init(initializer: initializer) + } /// 根据ViewModel和自定义的初始化代码创建一个Target并显示 /// - Parameters: /// - vm: 数据模型 /// - initializer: 初始化代码 - @discardableResult public required init(_ vm: ViewModel?, initializer: ((_ capsule: Target) -> Void)?) { - super.init(vm, initializer: initializer) + @discardableResult public convenience init(_ vm: ViewModel, initializer: ((_ capsule: Target) -> Void)?) { + self.init { capsule in + capsule.vm = vm + initializer?(capsule) + } } - @discardableResult public required convenience init(initializer: ((_ capsule: Target) -> Void)?) { - self.init(nil, initializer: initializer) + /// 根据ViewModel创建一个Target并显示 + /// - Parameter vm: 数据模型 + @discardableResult public convenience init(_ vm: ViewModel) { + self.init(vm, initializer: nil) } + @discardableResult public convenience init(_ text: String) { + self.init(.message(text), initializer: nil) + } + + /// 如果不存在就创建并弹出一个HUD实例,如果存在就更新实例 /// - Parameters: /// - identifier: 实例唯一标识符(如果为空,则以代码位置为唯一标识符) diff --git a/Sources/ProHUD/Capsule/CapsuleTarget.swift b/Sources/ProHUD/Capsule/CapsuleTarget.swift index ff465f3..dc56c33 100644 --- a/Sources/ProHUD/Capsule/CapsuleTarget.swift +++ b/Sources/ProHUD/Capsule/CapsuleTarget.swift @@ -46,7 +46,17 @@ open class CapsuleTarget: BaseController, HUDTargetType { return lb }() - public var vm: CapsuleViewModel = .init() + public var vm: CapsuleViewModel? + + public override var title: String? { + didSet { + if let vm = vm { + vm.title = title + } else { + vm = .title(title) + } + } + } private var tapActionCallback: ((_ capsule: CapsuleTarget) -> Void)? diff --git a/Sources/ProHUD/Capsule/CapsuleWindow.swift b/Sources/ProHUD/Capsule/CapsuleWindow.swift index 3d986f4..fea0a79 100644 --- a/Sources/ProHUD/Capsule/CapsuleWindow.swift +++ b/Sources/ProHUD/Capsule/CapsuleWindow.swift @@ -15,8 +15,8 @@ class CapsuleWindow: Window { self.capsule = capsule super.init(frame: .zero) windowScene = AppContext.windowScene - switch capsule.vm.position { - case .top: + switch capsule.vm?.position { + case .top, .none: // 略高于toast windowLevel = .phCapsuleTop case .middle: diff --git a/Sources/ProHUD/Core/Protocols/Provider.swift b/Sources/ProHUD/Core/Protocols/Provider.swift index 7246a4a..b25c7cb 100644 --- a/Sources/ProHUD/Core/Protocols/Provider.swift +++ b/Sources/ProHUD/Core/Protocols/Provider.swift @@ -12,11 +12,9 @@ public protocol HUDProviderType { associatedtype ViewModel = HUDViewModelType associatedtype Target = HUDTargetType - /// 根据ViewModel和自定义的初始化代码创建一个Target并显示 - /// - Parameters: - /// - vm: 数据模型 - /// - initializer: 初始化代码 - @discardableResult init(_ vm: ViewModel?, initializer: ((_ target: Target) -> Void)?) + /// 根据自定义的初始化代码创建一个Target并显示 + /// - Parameter initializer: 初始化代码 + @discardableResult init(initializer: ((_ target: Target) -> Void)?) } @@ -25,39 +23,22 @@ open class HUDProvider: HUDP /// HUD实例 public var target: Target - /// 根据ViewModel和自定义的初始化代码创建一个Target并显示 - /// - Parameters: - /// - vm: 数据模型 - /// - initializer: 初始化代码 - @discardableResult public required init(_ vm: ViewModel?, initializer: ((_ target: Target) -> Void)?) { + /// 根据自定义的初始化代码创建一个Target并显示 + /// - Parameter initializer: 初始化代码 + @discardableResult public required init(initializer: ((_ target: Target) -> Void)?) { var t = Target() - if let vm = vm as? Target.ViewModel { - t.vm = vm - } initializer?(t) self.target = t - if (vm == nil && initializer == nil) == false { + if (t.vm == nil && initializer == nil) == false { DispatchQueue.main.async { t.push() } } } - /// 根据自定义的初始化代码创建一个Target并显示 - /// - Parameter initializer: 初始化代码 - @discardableResult public convenience init(initializer: ((_ target: Target) -> Void)?) { - self.init(nil, initializer: initializer) - } - - /// 根据ViewModel创建一个Target并显示 - /// - Parameter vm: 数据模型 - @discardableResult public convenience init(_ vm: ViewModel?) { - self.init(vm, initializer: nil) - } - /// 创建一个空白的实例,不立即显示,需要手动调用target.push()来显示 @discardableResult public convenience init() { - self.init(nil, initializer: nil) + self.init(initializer: nil) } diff --git a/Sources/ProHUD/Core/Protocols/Target.swift b/Sources/ProHUD/Core/Protocols/Target.swift index 82749e2..db3ec52 100644 --- a/Sources/ProHUD/Core/Protocols/Target.swift +++ b/Sources/ProHUD/Core/Protocols/Target.swift @@ -14,6 +14,6 @@ import UIKit public protocol HUDTargetType: HUDControllerType { associatedtype ViewModel = HUDViewModelType - var vm: ViewModel { get set } + var vm: ViewModel? { get set } init() } diff --git a/Sources/ProHUD/Core/Utils/AppContext.swift b/Sources/ProHUD/Core/Utils/AppContext.swift index 6db9658..88c1230 100644 --- a/Sources/ProHUD/Core/Utils/AppContext.swift +++ b/Sources/ProHUD/Core/Utils/AppContext.swift @@ -36,7 +36,7 @@ public struct AppContext { static var toastWindows: [UIWindowScene: [ToastWindow]] = [:] static var alertWindow: [UIWindowScene: AlertWindow] = [:] static var sheetWindows: [UIWindowScene: [SheetWindow]] = [:] - static var capsuleWindows: [UIWindowScene: [CapsuleTarget.ViewModel.Position: CapsuleWindow]] = [:] + static var capsuleWindows: [UIWindowScene: [CapsuleViewModel.Position: CapsuleWindow]] = [:] static var current: AppContext? { guard let windowScene = windowScene else { return nil } @@ -120,7 +120,7 @@ extension AppContext { var toastWindows: [ToastWindow] { Self.toastWindows[windowScene] ?? [] } - var capsuleWindows: [CapsuleTarget.ViewModel.Position: CapsuleWindow] { + var capsuleWindows: [CapsuleViewModel.Position: CapsuleWindow] { Self.capsuleWindows[windowScene] ?? [:] } } diff --git a/Sources/ProHUD/Sheet/SheetDefaultLayout.swift b/Sources/ProHUD/Sheet/SheetDefaultLayout.swift index 15a89e3..6b34277 100644 --- a/Sources/ProHUD/Sheet/SheetDefaultLayout.swift +++ b/Sources/ProHUD/Sheet/SheetDefaultLayout.swift @@ -17,7 +17,7 @@ extension SheetTarget: DefaultLayout { if self.cfg.customReloadData?(self) == true { return } - view.tintColor = vm.tintColor ?? config.tintColor + view.tintColor = vm?.tintColor ?? config.tintColor // background if backgroundView.superview == nil { view.insertSubview(backgroundView, at: 0) diff --git a/Sources/ProHUD/Sheet/SheetProvider.swift b/Sources/ProHUD/Sheet/SheetProvider.swift index 8e5a9ec..dd15e43 100644 --- a/Sources/ProHUD/Sheet/SheetProvider.swift +++ b/Sources/ProHUD/Sheet/SheetProvider.swift @@ -8,12 +8,12 @@ import UIKit open class SheetProvider: HUDProvider { - @discardableResult public required init(_ vm: ViewModel?, initializer: ((_ sheet: Target) -> Void)?) { - super.init(vm, initializer: initializer) - } - @discardableResult public required convenience init(initializer: ((_ sheet: Target) -> Void)?) { - self.init(nil, initializer: initializer) + public typealias ViewModel = SheetViewModel + public typealias Target = SheetTarget + + @discardableResult @objc public required init(initializer: ((_ sheet: Target) -> Void)?) { + super.init(initializer: initializer) } /// 如果不存在就创建并弹出一个HUD实例,如果存在就更新实例 diff --git a/Sources/ProHUD/Sheet/SheetTarget.swift b/Sources/ProHUD/Sheet/SheetTarget.swift index 738455a..5fc245f 100644 --- a/Sources/ProHUD/Sheet/SheetTarget.swift +++ b/Sources/ProHUD/Sheet/SheetTarget.swift @@ -41,7 +41,7 @@ open class SheetTarget: BaseController, HUDTargetType { } } - public var vm: SheetViewModel = .init() + public var vm: SheetViewModel? = nil required public override init() { super.init() diff --git a/Sources/ProHUD/Toast/ToastConfiguration.swift b/Sources/ProHUD/Toast/ToastConfiguration.swift index 7ac28e1..d9f34f5 100644 --- a/Sources/ProHUD/Toast/ToastConfiguration.swift +++ b/Sources/ProHUD/Toast/ToastConfiguration.swift @@ -9,16 +9,6 @@ import UIKit public class ToastConfiguration: CommonConfiguration { - /// 元素与元素之间的距离 - public var margin = CGFloat(8) - - var customInfoStack: ((_ stack: StackView) -> Void)? - public func customInfoStack(handler: @escaping (_ stack: StackView) -> Void) { - customInfoStack = handler - } - /// 行间距 - public var lineSpace = CGFloat(4) - static var customGlobalConfig: ((_ config: ToastConfiguration) -> Void)? /// 全局共享配置(只能设置一次,影响所有实例) @@ -27,11 +17,21 @@ public class ToastConfiguration: CommonConfiguration { customGlobalConfig = callback } - /// 距离窗口左右的间距 - public var windowEdgeInset: CGFloat? - var windowEdgeInsetByDefault: CGFloat { - windowEdgeInset ?? 16 + /// 默认的持续时间 + public var defaultDuration: TimeInterval = 10 + + /// 元素与左右屏幕之间的距离(在没有达到最大宽度的情况下) + public var marginX = CGFloat(8) + + /// 元素与元素之间的纵向距离 + public var marginY = CGFloat(8) + + var customInfoStack: ((_ stack: StackView) -> Void)? + public func customInfoStack(handler: @escaping (_ stack: StackView) -> Void) { + customInfoStack = handler } + /// 行间距 + public var lineSpace = CGFloat(4) override var cardMaxWidthByDefault: CGFloat { cardMaxWidth ?? 500 diff --git a/Sources/ProHUD/Toast/ToastDefaultLayout.swift b/Sources/ProHUD/Toast/ToastDefaultLayout.swift index 7c9f543..4e6decb 100644 --- a/Sources/ProHUD/Toast/ToastDefaultLayout.swift +++ b/Sources/ProHUD/Toast/ToastDefaultLayout.swift @@ -17,7 +17,7 @@ extension ToastTarget: DefaultLayout { if self.cfg.customReloadData?(self) == true { return } - view.tintColor = vm.tintColor ?? config.tintColor + view.tintColor = vm?.tintColor ?? config.tintColor loadContentViewIfNeeded() loadContentMaskViewIfNeeded() guard customView == nil else { @@ -26,7 +26,7 @@ extension ToastTarget: DefaultLayout { } return } - if vm.icon != nil || vm.iconURL != nil { + if vm?.icon != nil || vm?.iconURL != nil { if imageView.superview == nil { infoStack.insertArrangedSubview(imageView, at: 0) imageView.snp.makeConstraints { make in @@ -42,8 +42,8 @@ extension ToastTarget: DefaultLayout { if textStack.superview == nil { infoStack.addArrangedSubview(textStack) } - let titleCount = vm.title?.count ?? 0 - let bodyCount = vm.message?.count ?? 0 + let titleCount = vm?.title?.count ?? 0 + let bodyCount = vm?.message?.count ?? 0 if titleCount > 0 { textStack.insertArrangedSubview(titleLabel, at: 0) if bodyCount > 0 { @@ -79,14 +79,12 @@ extension ToastTarget: DefaultLayout { bodyLabel.removeFromSuperview() } // 设置数据 - titleLabel.text = vm.title - bodyLabel.text = vm.message + titleLabel.text = vm?.title + bodyLabel.text = vm?.message view.layoutIfNeeded() // 设置持续时间 - vm.timeoutHandler = DispatchWorkItem(block: { [weak self] in - self?.pop() - }) + updateTimeoutDuration() setupImageView() @@ -132,6 +130,17 @@ extension ToastTarget { } } + private func updateTimeoutDuration() { + // 为空时使用默认规则 + if vm?.duration == nil { + vm?.duration = config.defaultDuration + } + // 设置持续时间 + vm?.timeoutHandler = DispatchWorkItem(block: { [weak self] in + self?.pop() + }) + } + func setupImageView() { // 移除动画 stopRotate(animateLayer) @@ -141,11 +150,11 @@ extension ToastTarget { // 移除进度 progressView?.removeFromSuperview() - imageView.image = vm.icon - if let iconURL = vm.iconURL { + imageView.image = vm?.icon + if let iconURL = vm?.iconURL { config.customWebImage?(imageView, iconURL) } - if let rotation = vm.rotation { + if let rotation = vm?.rotation { startRotate(rotation) } diff --git a/Sources/ProHUD/Toast/ToastManager.swift b/Sources/ProHUD/Toast/ToastManager.swift index 631589e..52b31f7 100644 --- a/Sources/ProHUD/Toast/ToastManager.swift +++ b/Sources/ProHUD/Toast/ToastManager.swift @@ -50,7 +50,7 @@ extension ToastTarget { // frame let cardEdgeInsets = config.cardEdgeInsetsByDefault - let width = CGFloat.minimum(AppContext.appBounds.width - config.windowEdgeInsetByDefault - config.windowEdgeInsetByDefault, config.cardMaxWidthByDefault) + let width = CGFloat.minimum(AppContext.appBounds.width - config.marginX - config.marginX, config.cardMaxWidthByDefault) view.frame.size = CGSize(width: width, height: config.cardMaxHeightByDefault) titleLabel.sizeToFit() bodyLabel.sizeToFit() @@ -92,7 +92,7 @@ extension ToastTarget { } else { consolePrint("‼️代码漏洞:已经没有toast了") } - vm.duration = nil + vm?.duration = nil setContextWindows(windows) UIView.animateEaseOut(duration: config.animateDurationForBuildOutByDefault) { window.transform = .init(translationX: 0, y: 0-20-window.maxY) @@ -124,21 +124,34 @@ fileprivate var updateToastsLayoutWorkItem: DispatchWorkItem? fileprivate extension ToastWindow { static func setToastWindowsLayout(windows: [ToastWindow]) { + var windows: [Window] = windows + if let win = AppContext.current?.capsuleWindows[.top] { + windows.insert(win, at: 0) + } for (i, window) in windows.enumerated() { - let config = window.toast.config + let margin: CGFloat + if let window = window as? ToastWindow { + margin = window.toast.config.marginY + } else if let window = window as? CapsuleWindow { + margin = window.safeAreaInsets.top + } else { + margin = 8 + } var y = window.frame.origin.y if i == 0 { - let topLayoutMargins = AppContext.appWindow?.layoutMargins.top ?? config.margin - y = max(topLayoutMargins - config.margin, config.margin) + let topLayoutMargins = AppContext.appWindow?.safeAreaInsets.top ?? margin + y = max(topLayoutMargins, margin) } else { if i - 1 < windows.count && i > 0 { - y = config.margin + windows[i-1].frame.maxY + y = margin + windows[i-1].frame.maxY } else { - y = config.margin + y = margin } } - window.maxY = y + window.frame.size.height - UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) { + if let window = window as? ToastWindow { + window.maxY = y + window.frame.size.height + } + UIView.animateEaseOut(duration: 0.68) { window.frame.origin.y = y } } @@ -154,3 +167,10 @@ fileprivate extension ToastWindow { } } + +extension ToastWindow { + static func updateToastWindowsLayout() { + let wins = AppContext.current?.toastWindows ?? [] + updateToastWindowsLayout(windows: wins) + } +} diff --git a/Sources/ProHUD/Toast/ToastProvider.swift b/Sources/ProHUD/Toast/ToastProvider.swift index b726549..e1c2c22 100644 --- a/Sources/ProHUD/Toast/ToastProvider.swift +++ b/Sources/ProHUD/Toast/ToastProvider.swift @@ -8,19 +8,37 @@ import UIKit open class ToastProvider: HUDProvider { - @discardableResult public required init(_ vm: ViewModel?, initializer: ((_ toast: Target) -> Void)?) { - super.init(vm, initializer: initializer) + + public typealias ViewModel = ToastViewModel + public typealias Target = ToastTarget + + @discardableResult @objc public required init(initializer: ((_ toast: Target) -> Void)?) { + super.init(initializer: initializer) } - @discardableResult public required convenience init(initializer: ((_ toast: Target) -> Void)?) { - self.init(nil, initializer: initializer) + @discardableResult public convenience init(_ vm: ViewModel, initializer: ((_ toast: Target) -> Void)?) { + self.init { toast in + toast.vm = vm + initializer?(toast) + } } + /// 根据ViewModel创建一个Target并显示 + /// - Parameter vm: 数据模型 + @discardableResult public convenience init(_ vm: ViewModel) { + self.init(vm, initializer: nil) + } + + @discardableResult @objc public convenience init(_ text: String) { + self.init(.message(text), initializer: nil) + } + + /// 如果不存在就创建并弹出一个HUD实例,如果存在就更新实例 /// - Parameters: /// - identifier: 实例唯一标识符(如果为空,则以代码位置为唯一标识符) /// - handler: 实例创建代码 - public static func lazyPush(identifier: String? = nil, file: String = #file, line: Int = #line, handler: @escaping (_ toast: ToastTarget) -> Void, onExists: ((_ toast: ToastTarget) -> Void)? = nil) { + @objc public static func lazyPush(identifier: String? = nil, file: String = #file, line: Int = #line, handler: @escaping (_ toast: ToastTarget) -> Void, onExists: ((_ toast: ToastTarget) -> Void)? = nil) { let id = identifier ?? (file + "#\(line)") if let vc = find(identifier: id).last { vc.update(handler: onExists ?? handler) @@ -35,7 +53,7 @@ open class ToastProvider: HUDProvider { /// 查找HUD实例 /// - Parameter identifier: 唯一标识符 /// - Returns: HUD实例 - @discardableResult public static func find(identifier: String, update handler: ((_ toast: ToastTarget) -> Void)? = nil) -> [ToastTarget] { + @discardableResult @objc public static func find(identifier: String, update handler: ((_ toast: ToastTarget) -> Void)? = nil) -> [ToastTarget] { let arr = AppContext.toastWindows.values.flatMap({ $0 }).compactMap({ $0.toast }).filter({ $0.identifier == identifier }) if let handler = handler { arr.forEach({ $0.update(handler: handler) }) diff --git a/Sources/ProHUD/Toast/ToastTarget.swift b/Sources/ProHUD/Toast/ToastTarget.swift index b94dd70..9b875d9 100644 --- a/Sources/ProHUD/Toast/ToastTarget.swift +++ b/Sources/ProHUD/Toast/ToastTarget.swift @@ -84,14 +84,17 @@ open class ToastTarget: BaseController, HUDTargetType { public var isRemovable = true /// 视图模型 - @objc public var vm = ToastViewModel() + @objc public var vm: ToastViewModel? private var tapActionCallback: ((_ toast: ToastTarget) -> Void)? - public override var title: String? { didSet { - vm.title = title + if let vm = vm { + vm.title = title + } else { + vm = .title(title) + } } } @@ -135,7 +138,7 @@ fileprivate extension ToastTarget { /// 拖拽事件 /// - Parameter sender: 手势 @objc func _onPanGesture(_ sender: UIPanGestureRecognizer) { - vm.timeoutTimer?.invalidate() + vm?.timeoutTimer?.invalidate() let point = sender.translation(in: sender.view) window?.transform = .init(translationX: 0, y: point.y) if sender.state == .recognized { @@ -150,8 +153,8 @@ fileprivate extension ToastTarget { UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) { self.window?.transform = .identity } completion: { done in - let d = self.vm.duration - self.vm.duration = d + let d = self.vm?.duration + self.vm?.duration = d } } }