From cbd654d80bb4fd4394376348afdc05173e85728d Mon Sep 17 00:00:00 2001 From: xaoxuu Date: Tue, 22 Aug 2023 02:00:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHDemo/PHDemo/DemoAlertVC.swift | 3 +- PHDemo/PHDemo/DemoCapsuleVC.swift | 213 +++++++++++++++--- PHDemo/PHDemo/DemoSheetVC.swift | 13 +- PHDemo/PHDemo/DemoToastVC.swift | 65 +++++- Sources/ProHUD/Alert/AlertConfiguration.swift | 7 +- .../ProHUD/Alert/AlertConvenienceLayout.swift | 4 +- Sources/ProHUD/Alert/AlertDefaultLayout.swift | 8 +- Sources/ProHUD/Alert/AlertManager.swift | 22 +- Sources/ProHUD/Alert/AlertProvider.swift | 53 ++--- Sources/ProHUD/Alert/AlertTarget.swift | 6 +- .../ProHUD/Capsule/CapsuleConfiguration.swift | 7 +- .../ProHUD/Capsule/CapsuleDefaultLayout.swift | 8 +- Sources/ProHUD/Capsule/CapsuleManager.swift | 37 ++- Sources/ProHUD/Capsule/CapsuleProvider.swift | 55 ++--- Sources/ProHUD/Capsule/CapsuleTarget.swift | 13 +- Sources/ProHUD/Capsule/CapsuleWindow.swift | 1 + .../Core/Controllers/BaseController.swift | 6 +- .../ProHUD/Core/Models/BaseViewModel.swift | 53 +++-- .../ProHUD/Core/Models/Configuration.swift | 2 +- Sources/ProHUD/Core/Protocols/Provider.swift | 18 -- Sources/ProHUD/Core/Protocols/Target.swift | 1 + .../ProHUD/Core/Utils/RotateAnimation.swift | 2 +- Sources/ProHUD/Core/Utils/ViewExts.swift | 4 +- Sources/ProHUD/Sheet/SheetConfiguration.swift | 7 +- .../ProHUD/Sheet/SheetConvenienceLayout.swift | 4 +- Sources/ProHUD/Sheet/SheetDefaultLayout.swift | 2 +- Sources/ProHUD/Sheet/SheetManager.swift | 2 + Sources/ProHUD/Sheet/SheetProvider.swift | 21 +- Sources/ProHUD/Sheet/SheetTarget.swift | 6 +- Sources/ProHUD/Toast/ToastConfiguration.swift | 7 +- .../ProHUD/Toast/ToastConvenienceLayout.swift | 4 +- Sources/ProHUD/Toast/ToastDefaultLayout.swift | 6 +- Sources/ProHUD/Toast/ToastManager.swift | 24 +- Sources/ProHUD/Toast/ToastProvider.swift | 53 ++--- Sources/ProHUD/Toast/ToastTarget.swift | 11 +- 35 files changed, 481 insertions(+), 267 deletions(-) diff --git a/PHDemo/PHDemo/DemoAlertVC.swift b/PHDemo/PHDemo/DemoAlertVC.swift index 330af50..2ab055f 100644 --- a/PHDemo/PHDemo/DemoAlertVC.swift +++ b/PHDemo/PHDemo/DemoAlertVC.swift @@ -311,13 +311,12 @@ class DemoAlertVC: ListVC { } section.add(title: "弹出loading,如果已经存在就更新") { func f(i: Int) { - Alert.lazyPush(identifier: "haha") { alert in + Alert(.identifier("haha")) { alert in if i < 2 { alert.vm = .loading.title("第\(i)次弹") let btn = alert.add(action: "请稍等", identifier: "btn") btn.isEnabled = false } else { - alert.update(progress: 1) alert.vm = .success.title("第\(i)次弹").message("只更新内容") alert.reloadTextStack() alert.update(action: "完成", style: .filled(color: .systemGreen), for: "btn") diff --git a/PHDemo/PHDemo/DemoCapsuleVC.swift b/PHDemo/PHDemo/DemoCapsuleVC.swift index 8bf9bc4..c941877 100644 --- a/PHDemo/PHDemo/DemoCapsuleVC.swift +++ b/PHDemo/PHDemo/DemoCapsuleVC.swift @@ -84,15 +84,15 @@ class DemoCapsuleVC: ListVC { } } - list.add(title: "不同位置、不同动画,队列推送") { section in + list.add(title: "不同位置、不同动画") { section in section.add(title: "顶部,默认动画") { - Capsule(.info("一条简短的消息").queuedPush(true).duration(1)) + Capsule(.info("一条简短的消息").duration(1)) } section.add(title: "中间,默认动画") { - Capsule(.middle.queuedPush(true).info("一条简短的消息").duration(2)) + Capsule(.middle.info("一条简短的消息").duration(1)) } section.add(title: "中间,黑底白字,透明渐变") { - Capsule(.middle.queuedPush(true).info("一条简短的消息").duration(1)) { capsule in + Capsule(.middle.info("一条简短的消息").duration(1)) { capsule in capsule.config.tintColor = .white capsule.config.cardCornerRadius = 8 capsule.config.contentViewMask { mask in @@ -119,22 +119,25 @@ class DemoCapsuleVC: ListVC { } } section.add(title: "底部,渐变背景,默认回弹滑入") { - Capsule(.bottom.queuedPush(true).enter("点击进入").duration(1)) { capsule in + Capsule(.bottom.enter("点击进入").duration(2)) { capsule in capsule.config.tintColor = .white capsule.config.cardEdgeInsets = .init(top: 12, left: 20, bottom: 12, right: 20) capsule.config.customTextLabel { label in label.textColor = .white label.font = .boldSystemFont(ofSize: 16) } - capsule.config.contentViewMask { mask in + capsule.config.contentViewMask { [weak capsule] mask in mask.effect = .none mask.backgroundColor = .clear let gradientLayer = CAGradientLayer() - gradientLayer.frame = self.view.bounds + if let f = capsule?.view.bounds { + gradientLayer.frame = f + } else { + gradientLayer.frame = .init(x: 0, y: 0, width: 300, height: 100) + } gradientLayer.colors = [UIColor("#0091FF").cgColor, UIColor("#00FDFF").cgColor] gradientLayer.startPoint = .init(x: 0.2, y: 0.6) gradientLayer.endPoint = .init(x: 0.6, y: 0.2) - gradientLayer.frame = .init(x: 0, y: 0, width: 300, height: 100) mask.layer.sublayers?.forEach({ $0.removeFromSuperlayer() }) mask.layer.insertSublayer(gradientLayer, at: 0) } @@ -146,27 +149,157 @@ class DemoCapsuleVC: ListVC { } } } - list.add(title: "lazy push") { section in - section.add(title: "id:1, text:1") { - Capsule(.test1("111:111")) + list.add(title: "自定义子类,队列推送") { section in + section.add(title: "视频采集,title:开") { + GradientCapsule(.videoRecord(on: true, text: "视频采集:开")) } - section.add(title: "id:1, text:2") { - Capsule(.test1("111:222")) + section.add(title: "视频推流,title:开") { + GradientCapsule(.videoPush(on: true, text: "视频推流:开")) } - section.add(title: "id:2, text:1") { - Capsule(.test2("222:111")) + section.add(title: "音频采集,title:开") { + GradientCapsule(.audioRecord(on: true, text: "音频采集:开")) } - section.add(title: "id:2, text:2") { - Capsule(.test2("222:222")) + section.add(title: "音频推流,title:开") { + GradientCapsule(.audioPush(on: true, text: "音频推流:开")) } - section.add(title: "id:2, text:2") { - Capsule(.test2("222:222")) - Capsule(.test2("222:111")) + section.add(title: "视频采集,title:关") { + GradientCapsule(.videoRecord(on: false, text: "视频采集:关")) + } + section.add(title: "视频推流,title:关") { + GradientCapsule(.videoPush(on: false, text: "视频推流:关")) + } + section.add(title: "音频采集,title:关") { + GradientCapsule(.audioRecord(on: false, text: "音频采集:关")) + } + section.add(title: "音频推流,title:关") { + GradientCapsule(.audioPush(on: false, text: "音频推流:关")) + } + } + + var i = 0 + list.add(title: "lazy push") { section in + section.add(title: "以当前代码位置作为唯一标识符") { + i += 1 + Capsule(.codeIdentifier().message("id=\(i)")) + } + section.add(title: "指定id=a, haha") { + Capsule(.identifier("a").message("id=a, haha")) + } + section.add(title: "指定id=a, hahaha") { + Capsule(.identifier("a").message("id=a, hahaha")) + } + } + + } + +} + +class GradientCapsuleTarget: CapsuleTarget { + + override var config: CapsuleConfiguration { + get { customConfig } + set { customConfig = newValue} + } + + private lazy var customConfig: CapsuleConfiguration = { + let cfg = CapsuleConfiguration() + cfg.tintColor = .white + cfg.customTextLabel { label in + label.textColor = .white + } + cfg.contentViewMask { [weak self] mask in + mask.effect = .none + mask.backgroundColor = .clear + let gradientLayer = CAGradientLayer() + if let f = self?.view.bounds { + gradientLayer.frame = f + } else { + gradientLayer.frame = .init(x: 0, y: 0, width: 300, height: 100) + } + gradientLayer.colors = [UIColor.systemGreen.cgColor, UIColor("#B6F598").cgColor] + gradientLayer.startPoint = .init(x: 0.2, y: 0.6) + gradientLayer.endPoint = .init(x: 0.6, y: 0.2) + mask.layer.sublayers?.forEach({ $0.removeFromSuperlayer() }) + mask.layer.insertSublayer(gradientLayer, at: 0) + self?.gradientLayer = gradientLayer + } + return cfg + }() + + var gradientLayer: CAGradientLayer? + + override func viewDidLoad() { + super.viewDidLoad() + let id = vm?.identifier ?? "" + if id.contains("video") == true { + if id.contains(":true") { + gradientLayer?.colors = [UIColor("#7EF19B").cgColor, UIColor.systemGreen.cgColor] + } else { + gradientLayer?.colors = [UIColor("#1A7531").cgColor, UIColor("#2A5133").cgColor] + } + } else { + if id.contains(":true") { + gradientLayer?.colors = [UIColor("#FFC470").cgColor, UIColor.systemOrange.cgColor] + } else { + gradientLayer?.colors = [UIColor("#8F5300").cgColor, UIColor("#51412A").cgColor] + } + } + + } +} + +class GradientCapsule: HUDProviderType { + + public typealias ViewModel = CapsuleViewModel + public typealias Target = GradientCapsuleTarget + + /// 根据自定义的初始化代码创建一个Target并显示 + /// - Parameter initializer: 初始化代码(传空值时不会做任何事) + @discardableResult required init(initializer: ((_ target: Target) -> Void)?) { + guard let initializer = initializer else { + // Provider的作用就是push一个target + // 如果没有任何初始化代码就没有target,就是个无意义的Provider + // 但为了支持lazyPush(找到已有实例并更新),所以就需要支持无意义的Provider + // 详见子类中的 self.init(initializer: nil) + return + } + let t = Target() + initializer(t) + t.push() + } + + /// 根据ViewModel和自定义的初始化代码创建一个Target并显示 + /// - Parameters: + /// - vm: 数据模型 + /// - initializer: 初始化代码 + @discardableResult public convenience init(_ vm: ViewModel, initializer: ((_ capsule: Target) -> Void)?) { + if let id = vm.identifier, id.count > 0, let target = CapsuleManager.find(identifier: id).last as? Target { + target.update { capsule in + capsule.vm = vm + initializer?(capsule as! GradientCapsule.Target) + } + self.init(initializer: nil) + } else { + self.init { target in + target.vm = vm + initializer?(target) } } } - + + /// 根据ViewModel创建一个Target并显示 + /// - Parameter vm: 数据模型 + @discardableResult public convenience init(_ vm: ViewModel) { + self.init(vm, initializer: nil) + } + + /// 根据文本作为数据模型创建一个Target并显示 + /// - Parameter text: 文本 + @discardableResult public convenience init(_ text: String) { + self.init(.message(text), initializer: nil) + } + } extension CapsuleViewModel { @@ -198,22 +331,40 @@ extension CapsuleViewModel { .message(text) } - static func test1(_ text: String) -> CapsuleViewModel { - .identifier("id:1") - .icon(.init(systemName: "video.circle.fill")) - .tintColor(.systemGreen) - .duration(1) + static func videoRecord(on: Bool, text: String) -> CapsuleViewModel { + .middle + .identifier("video.record:\(on)") + .icon(.init(systemName: on ? "video.fill" : "video.slash.fill")) + .duration(0.5) .queuedPush(true) .message(text) } - static func test2(_ text: String) -> CapsuleViewModel { - .identifier("id:2") - .icon(.init(systemName: "mic.circle.fill")) - .tintColor(.systemOrange) - .duration(1) + static func audioRecord(on: Bool, text: String) -> CapsuleViewModel { + .middle + .identifier("audio.record:\(on)") + .icon(.init(systemName: on ? "mic.fill" : "mic.slash.fill")) + .duration(0.5) .queuedPush(true) .message(text) } + + static func videoPush(on: Bool, text: String) -> CapsuleViewModel { + .middle + .identifier("video.push:\(on)") + .icon(.init(systemName: on ? "icloud.fill" : "icloud.slash.fill")) + .duration(0.5) + .queuedPush(true) + .message(text) + } + + static func audioPush(on: Bool, text: String) -> CapsuleViewModel { + .middle + .identifier("audio.push:\(on)") + .icon(.init(systemName: on ? "icloud.fill" : "icloud.slash.fill")) + .duration(0.5) + .queuedPush(true) + .message(text) + } } diff --git a/PHDemo/PHDemo/DemoSheetVC.swift b/PHDemo/PHDemo/DemoSheetVC.swift index cbceca2..d9a1d65 100644 --- a/PHDemo/PHDemo/DemoSheetVC.swift +++ b/PHDemo/PHDemo/DemoSheetVC.swift @@ -74,12 +74,13 @@ class DemoSheetVC: ListVC { sheet.add(action: "确认") sheet.add(action: "取消", style: .gray) sheet.onTappedBackground { sheet in - Toast.lazyPush(identifier: "alert") { toast in - toast.vm = .error - .title("点击了背景") - .message("点击背景将不会dismiss,必须在下方做出选择才能关掉") - .duration(2) - } + Toast( + .error + .lazyIdentifier() + .title("点击了背景") + .message("点击背景将不会dismiss,必须在下方做出选择才能关掉") + .duration(2) + ) } } } diff --git a/PHDemo/PHDemo/DemoToastVC.swift b/PHDemo/PHDemo/DemoToastVC.swift index 5b689a7..748c7dc 100644 --- a/PHDemo/PHDemo/DemoToastVC.swift +++ b/PHDemo/PHDemo/DemoToastVC.swift @@ -34,14 +34,61 @@ let isTesting: Bool = true class TestToastTarget: ToastTarget { override func push() { + print("isTesting: \(isTesting)") guard isTesting else { return } super.push() } } -//typealias TestToast = HUDProvider -class TestToast: ToastProvider { - typealias Target = TestToastTarget +class TestToast: HUDProviderType { + + public typealias ViewModel = ToastViewModel + public typealias Target = TestToastTarget + + /// 根据自定义的初始化代码创建一个Target并显示 + /// - Parameter initializer: 初始化代码(传空值时不会做任何事) + @discardableResult public required init(initializer: ((_ target: Target) -> Void)?) { + guard let initializer = initializer else { + // Provider的作用就是push一个target + // 如果没有任何初始化代码就没有target,就是个无意义的Provider + // 但为了支持lazyPush(找到已有实例并更新),所以就需要支持无意义的Provider + // 详见子类中的 self.init(initializer: nil) + return + } + let t = Target() + initializer(t) + t.push() + } + + /// 根据ViewModel和自定义的初始化代码创建一个Target并显示 + /// - Parameters: + /// - vm: 数据模型 + /// - initializer: 自定义的初始化代码 + @discardableResult public convenience init(_ vm: ViewModel, initializer: ((_ toast: Target) -> Void)?) { + if let id = vm.identifier, id.count > 0, let target = ToastManager.find(identifier: id).last as? Target { + target.vm = vm + initializer?(target) + self.init(initializer: nil) + } else { + self.init { target in + target.vm = vm + initializer?(target) + } + } + } + + /// 根据ViewModel创建一个Target并显示 + /// - Parameter vm: 数据模型 + @discardableResult public convenience init(_ vm: ViewModel) { + self.init(vm, initializer: nil) + } + + /// 根据文本作为数据模型创建一个Target并显示 + /// - Parameter text: 文本 + @discardableResult @objc public convenience init(_ text: String) { + self.init(.message(text), initializer: nil) + } + } class DemoToastVC: ListVC { @@ -127,12 +174,12 @@ class DemoToastVC: ListVC { section.add(title: "增加按钮") { let title = "您收到了一条好友申请" let message = "丹妮莉丝·坦格利安申请添加您为好友,是否同意?" - Toast(.title(title).message(message).icon(.init(named: "avatar"))) { toast in + Toast(.title(title).message(message).icon(.init(named: "avatar")).duration(.infinity)) { toast in toast.isRemovable = false 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(.identifier("Dracarys")) { alert in alert.vm = .message("Dracarys") .icon(UIImage(inProHUD: "prohud.windmill")) .rotation(.init(repeatCount: .infinity)) @@ -152,13 +199,13 @@ class DemoToastVC: ListVC { } } toast.add(action: "同意") { toast in - Alert.find(identifier: "Dracarys", update: { alert in + AlertManager.find(identifier: "Dracarys", update: { alert in alert.pop() }) toast.pop() Alert(.success(1).message("Good choice!")) } - Toast.find(identifier: "loading") { toast in + ToastManager.find(identifier: "loading") { toast in toast.vm = .success(2).message("加载成功") } } @@ -190,12 +237,12 @@ class DemoToastVC: ListVC { } section.add(title: "如果存在就更新,如果不存在就忽略") { i += 1 - Toast.find(identifier: "loading") { toast in + ToastManager.find(identifier: "loading") { toast in toast.vm = .success.title("加载完成\(i)").message("这条消息不会重复显示多条").duration(2) } } section.add(title: "移除指定实例") { - Toast.find(identifier: "loading") { toast in + ToastManager.find(identifier: "loading") { toast in toast.pop() } } diff --git a/Sources/ProHUD/Alert/AlertConfiguration.swift b/Sources/ProHUD/Alert/AlertConfiguration.swift index c02e8a8..5987fb7 100644 --- a/Sources/ProHUD/Alert/AlertConfiguration.swift +++ b/Sources/ProHUD/Alert/AlertConfiguration.swift @@ -14,7 +14,12 @@ public class AlertConfiguration: CommonConfiguration { public var enableShadow: Bool = true - static var customGlobalConfig: ((_ config: AlertConfiguration) -> Void)? + private static var customGlobalConfig: ((_ config: AlertConfiguration) -> Void)? + + public override init() { + super.init() + Self.customGlobalConfig?(self) + } /// 全局共享配置(只能设置一次,影响所有实例) /// - Parameter callback: 配置代码 diff --git a/Sources/ProHUD/Alert/AlertConvenienceLayout.swift b/Sources/ProHUD/Alert/AlertConvenienceLayout.swift index 80706f1..742c85c 100644 --- a/Sources/ProHUD/Alert/AlertConvenienceLayout.swift +++ b/Sources/ProHUD/Alert/AlertConvenienceLayout.swift @@ -32,7 +32,7 @@ extension AlertTarget: InternalConvenienceLayout { self?.pop() } } - if isViewDisplayed { + if isViewAppeared { self.actionStack.layoutIfNeeded() UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) { self.view.layoutIfNeeded() @@ -78,7 +78,7 @@ extension AlertTarget: InternalConvenienceLayout { buttonEvents[view] = nil } } - if isViewDisplayed { + if isViewAppeared { UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) { self.actionStack.layoutIfNeeded() self.view.layoutIfNeeded() diff --git a/Sources/ProHUD/Alert/AlertDefaultLayout.swift b/Sources/ProHUD/Alert/AlertDefaultLayout.swift index 5b4032e..a1be0a6 100644 --- a/Sources/ProHUD/Alert/AlertDefaultLayout.swift +++ b/Sources/ProHUD/Alert/AlertDefaultLayout.swift @@ -26,8 +26,10 @@ extension AlertTarget: DefaultLayout { } else { isFirstLayout = false } - // 更新时间 - updateTimeoutDuration() + if isViewAppeared { + // 更新时间 + updateTimeoutDuration() + } // custom layout guard customView == nil else { return @@ -116,7 +118,7 @@ extension AlertTarget: DefaultLayout { } func setDefaultAxis() { - guard isViewDisplayed == false && config.customActionStack == nil else { return } + guard isViewAppeared == false && config.customActionStack == nil else { return } let count = actionStack.arrangedSubviews.filter({ $0.isKind(of: UIControl.self )}).count guard count < 4 else { return } if (isPortrait && count < 3) || !isPortrait { diff --git a/Sources/ProHUD/Alert/AlertManager.swift b/Sources/ProHUD/Alert/AlertManager.swift index 30ce4ac..30b60c2 100644 --- a/Sources/ProHUD/Alert/AlertManager.swift +++ b/Sources/ProHUD/Alert/AlertManager.swift @@ -24,6 +24,8 @@ extension AlertTarget { view.snp.makeConstraints { make in make.edges.equalToSuperview() } + // 为了更连贯,从进入动画开始时就开始计时 + updateTimeoutDuration() UIView.animateEaseOut(duration: config.animateDurationForBuildIn ?? config.animateDurationForBuildInByDefault) { self.view.transform = .identity self.view.alpha = 1 @@ -33,7 +35,6 @@ extension AlertTarget { window.backgroundView.alpha = 1 } completion: { done in self.navEvents[.onViewDidAppear]?(self) - self.updateTimeoutDuration() } window.alerts.append(self) AlertTarget.updateAlertsLayout(alerts: window.alerts) @@ -76,9 +77,7 @@ extension AlertTarget { func updateTimeoutDuration() { // 设置持续时间 - vm?.timeoutHandler = DispatchWorkItem(block: { [weak self] in - self?.pop() - }) + vm?.restartTimer() } } @@ -125,3 +124,18 @@ fileprivate extension AlertTarget { } + +public class AlertManager: NSObject { + + /// 查找HUD实例 + /// - Parameter identifier: 唯一标识符 + /// - Returns: HUD实例 + @discardableResult public static func find(identifier: String, update handler: ((_ alert: AlertTarget) -> Void)? = nil) -> [AlertTarget] { + let arr = AppContext.alertWindow.values.flatMap({ $0.alerts }).filter({ $0.identifier == identifier }) + if let handler = handler { + arr.forEach({ $0.update(handler: handler) }) + } + return arr + } + +} diff --git a/Sources/ProHUD/Alert/AlertProvider.swift b/Sources/ProHUD/Alert/AlertProvider.swift index 7837890..1b1f3a8 100644 --- a/Sources/ProHUD/Alert/AlertProvider.swift +++ b/Sources/ProHUD/Alert/AlertProvider.swift @@ -7,13 +7,24 @@ import UIKit -open class AlertProvider: HUDProvider { +public final class AlertProvider: HUDProviderType { public typealias ViewModel = AlertViewModel public typealias Target = AlertTarget - @discardableResult @objc public required init(initializer: ((_ alert: Target) -> Void)?) { - super.init(initializer: initializer) + /// 根据自定义的初始化代码创建一个Target并显示 + /// - Parameter initializer: 初始化代码(传空值时不会做任何事) + @discardableResult public required init(initializer: ((_ target: Target) -> Void)?) { + guard let initializer = initializer else { + // Provider的作用就是push一个target + // 如果没有任何初始化代码就没有target,就是个无意义的Provider + // 但为了支持lazyPush(找到已有实例并更新),所以就需要支持无意义的Provider + // 详见子类中的 self.init(initializer: nil) + return + } + let t = Target() + initializer(t) + t.push() } /// 根据ViewModel和自定义的初始化代码创建一个Target并显示 @@ -21,10 +32,10 @@ open class AlertProvider: HUDProvider { /// - vm: 数据模型 /// - initializer: 自定义的初始化代码 @discardableResult public convenience init(_ vm: ViewModel, initializer: ((_ alert: Target) -> Void)?) { - if let id = vm.identifier, id.count > 0 { - Self.lazyPush(identifier: id) { target in - target.vm = vm - initializer?(target) + if let id = vm.identifier, id.count > 0, let target = AlertManager.find(identifier: id).last { + target.update { t in + t.vm = vm + initializer?(t) } self.init(initializer: nil) } else { @@ -34,39 +45,13 @@ open class AlertProvider: HUDProvider { } } } + /// 根据ViewModel创建一个Target并显示 /// - Parameter vm: 数据模型 @discardableResult public convenience init(_ vm: ViewModel) { self.init(vm, initializer: nil) } - /// 如果不存在就创建并弹出一个HUD实例,如果存在就更新实例 - /// - Parameters: - /// - identifier: 实例唯一标识符(如果为空,则以代码位置为唯一标识符) - /// - handler: 实例创建代码 - public static func lazyPush(identifier: String? = nil, file: String = #file, line: Int = #line, handler: @escaping (_ alert: AlertTarget) -> Void, onExists: ((_ alert: AlertTarget) -> Void)? = nil) { - let id = identifier ?? (file + "#\(line)") - if let vc = find(identifier: id).last { - vc.update(handler: onExists ?? handler) - } else { - Self.init { alert in - alert.identifier = id - handler(alert) - } - } - } - - /// 查找HUD实例 - /// - Parameter identifier: 唯一标识符 - /// - Returns: HUD实例 - @discardableResult public static func find(identifier: String, update handler: ((_ alert: AlertTarget) -> Void)? = nil) -> [AlertTarget] { - let arr = AppContext.alertWindow.values.flatMap({ $0.alerts }).filter({ $0.identifier == identifier }) - if let handler = handler { - arr.forEach({ $0.update(handler: handler) }) - } - return arr - } - } public typealias Alert = AlertProvider diff --git a/Sources/ProHUD/Alert/AlertTarget.swift b/Sources/ProHUD/Alert/AlertTarget.swift index 3149e53..eb7b3b8 100644 --- a/Sources/ProHUD/Alert/AlertTarget.swift +++ b/Sources/ProHUD/Alert/AlertTarget.swift @@ -11,11 +11,7 @@ open class AlertTarget: BaseController, HUDTargetType { public typealias ViewModel = AlertViewModel - public lazy var config: AlertConfiguration = { - var cfg = AlertConfiguration() - AlertConfiguration.customGlobalConfig?(cfg) - return cfg - }() + public lazy var config = AlertConfiguration() public var progressView: ProgressView? diff --git a/Sources/ProHUD/Capsule/CapsuleConfiguration.swift b/Sources/ProHUD/Capsule/CapsuleConfiguration.swift index ff42e1e..22b855f 100644 --- a/Sources/ProHUD/Capsule/CapsuleConfiguration.swift +++ b/Sources/ProHUD/Capsule/CapsuleConfiguration.swift @@ -11,7 +11,12 @@ public class CapsuleConfiguration: CommonConfiguration { public typealias CustomAnimateHandler = ((_ window: UIWindow, _ completion: @escaping () -> Void) -> Void) - static var customGlobalConfig: ((_ config: CapsuleConfiguration) -> Void)? + private static var customGlobalConfig: ((_ config: CapsuleConfiguration) -> Void)? + + public override init() { + super.init() + Self.customGlobalConfig?(self) + } /// 全局共享配置(只能设置一次,影响所有实例) /// - Parameter callback: 配置代码 diff --git a/Sources/ProHUD/Capsule/CapsuleDefaultLayout.swift b/Sources/ProHUD/Capsule/CapsuleDefaultLayout.swift index bf0bc11..86fd8b8 100644 --- a/Sources/ProHUD/Capsule/CapsuleDefaultLayout.swift +++ b/Sources/ProHUD/Capsule/CapsuleDefaultLayout.swift @@ -43,10 +43,9 @@ extension CapsuleTarget: DefaultLayout { view.layoutIfNeeded() - // 更新时间 - updateTimeoutDuration() - - if isViewDisplayed { + if isViewAppeared { + // 更新时间 + updateTimeoutDuration() UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) { self.view.layoutIfNeeded() } @@ -102,4 +101,5 @@ extension CapsuleTarget: DefaultLayout { } } + } diff --git a/Sources/ProHUD/Capsule/CapsuleManager.swift b/Sources/ProHUD/Capsule/CapsuleManager.swift index e23be22..0da93a1 100644 --- a/Sources/ProHUD/Capsule/CapsuleManager.swift +++ b/Sources/ProHUD/Capsule/CapsuleManager.swift @@ -35,6 +35,7 @@ extension CapsuleTarget { // 直接覆盖 isNew = false window = w + window.capsule = self } } else { // 空闲状态下推送一个新的 @@ -42,7 +43,7 @@ extension CapsuleTarget { window = CapsuleWindow(capsule: self) windows[position] = nil } - + window.isUserInteractionEnabled = tapActionCallback != nil // frame let cardEdgeInsetsByDefault = config.cardEdgeInsetsByDefault view.layoutIfNeeded() @@ -83,10 +84,10 @@ extension CapsuleTarget { // 更新toast防止重叠 ToastWindow.updateToastWindowsLayout() } - + // 为了更连贯,从进入动画开始时就开始计时 + updateTimeoutDuration() func completion() { self.navEvents[.onViewDidAppear]?(self) - self.updateTimeoutDuration() } if isNew { window.isHidden = false @@ -149,10 +150,10 @@ extension CapsuleTarget { window.transform = .identity self.navEvents[.onViewDidDisappear]?(self) } + var duration = config.animateDurationForBuildOutByDefault if let animateBuildOut = config.animateBuildOut { animateBuildOut(window, completion) } else { - let duration = config.animateDurationForBuildOutByDefault let oldFrame = window.frame switch position { case .top: @@ -162,13 +163,13 @@ extension CapsuleTarget { completion() } case .middle: - let duration = config.animateDurationForBuildInByDefault * 1 + duration = config.animateDurationForBuildInByDefault UIView.animateEaseIn(duration: duration) { window.transform = .init(translationX: 0, y: -24) } completion: { done in completion() } - UIView.animateLinear(duration: duration * 0.5, delay: duration * 0.3) { + UIView.animateEaseIn(duration: duration * 0.5, delay: duration * 0.5) { window.alpha = 0 } case .bottom: @@ -182,7 +183,8 @@ extension CapsuleTarget { } if let next = AppContext.capsuleInQueue.first(where: { $0.preferredWindowScene == windowScene && $0.vm?.position == position }) { AppContext.capsuleInQueue.removeAll(where: { $0 == next }) - DispatchQueue.main.asyncAfter(deadline: .now() + config.animateDurationForBuildOutByDefault * 0.8) { + // 在这个pop的同时push下一个 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { next.push() } } @@ -205,9 +207,24 @@ extension CapsuleTarget { vm?.duration = config.defaultDuration } // 设置持续时间 - vm?.timeoutHandler = DispatchWorkItem(block: { [weak self] in - self?.pop() - }) + vm?.restartTimer() + } + +} + +public class CapsuleManager: NSObject { + + /// 查找HUD实例 + /// - Parameter identifier: 唯一标识符 + /// - Returns: HUD实例 + @discardableResult public static func find(identifier: String, update handler: ((_ capsule: CapsuleTarget) -> Void)? = nil) -> [CapsuleTarget] { + let allPositions = AppContext.capsuleWindows.values.flatMap({ $0.values }) + let allCapsules = allPositions.compactMap({ $0.capsule }) + let arr = (allCapsules + AppContext.capsuleInQueue).filter({ $0.identifier == identifier || $0.vm?.identifier == identifier }) + if let handler = handler { + arr.forEach({ $0.update(handler: handler) }) + } + return arr } } diff --git a/Sources/ProHUD/Capsule/CapsuleProvider.swift b/Sources/ProHUD/Capsule/CapsuleProvider.swift index f4e57f0..5047676 100644 --- a/Sources/ProHUD/Capsule/CapsuleProvider.swift +++ b/Sources/ProHUD/Capsule/CapsuleProvider.swift @@ -7,13 +7,24 @@ import UIKit -open class CapsuleProvider: HUDProvider { +public final class CapsuleProvider: HUDProviderType { public typealias ViewModel = CapsuleViewModel public typealias Target = CapsuleTarget - @discardableResult @objc public required init(initializer: ((_ capsule: Target) -> Void)?) { - super.init(initializer: initializer) + /// 根据自定义的初始化代码创建一个Target并显示 + /// - Parameter initializer: 初始化代码(传空值时不会做任何事) + @discardableResult public required init(initializer: ((_ target: Target) -> Void)?) { + guard let initializer = initializer else { + // Provider的作用就是push一个target + // 如果没有任何初始化代码就没有target,就是个无意义的Provider + // 但为了支持lazyPush(找到已有实例并更新),所以就需要支持无意义的Provider + // 详见子类中的 self.init(initializer: nil) + return + } + let t = Target() + initializer(t) + t.push() } /// 根据ViewModel和自定义的初始化代码创建一个Target并显示 @@ -21,10 +32,10 @@ open class CapsuleProvider: HUDProvider { /// - vm: 数据模型 /// - initializer: 初始化代码 @discardableResult public convenience init(_ vm: ViewModel, initializer: ((_ capsule: Target) -> Void)?) { - if let id = vm.identifier, id.count > 0 { - Self.lazyPush(identifier: id) { target in - target.vm = vm - initializer?(target) + if let id = vm.identifier, id.count > 0, let target = CapsuleManager.find(identifier: id).last { + target.update { t in + t.vm = vm + initializer?(t) } self.init(initializer: nil) } else { @@ -47,36 +58,6 @@ open class CapsuleProvider: HUDProvider { 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 (_ capsule: CapsuleTarget) -> Void, onExists: ((_ capsule: CapsuleTarget) -> Void)? = nil) { - let id = identifier ?? (file + "#\(line)") - if let vc = find(identifier: id).last { - vc.update(handler: onExists ?? handler) - } else { - Self.init { capsule in - capsule.identifier = id - handler(capsule) - } - } - } - - /// 查找HUD实例 - /// - Parameter identifier: 唯一标识符 - /// - Returns: HUD实例 - @discardableResult public static func find(identifier: String, update handler: ((_ capsule: CapsuleTarget) -> Void)? = nil) -> [CapsuleTarget] { - let allPositions = AppContext.capsuleWindows.values.flatMap({ $0.values }) - let allCapsules = allPositions.compactMap({ $0.capsule }) - let arr = (allCapsules + AppContext.capsuleInQueue).filter({ $0.identifier == identifier }) - if let handler = handler { - arr.forEach({ $0.update(handler: handler) }) - } - return arr - } - } public typealias Capsule = CapsuleProvider diff --git a/Sources/ProHUD/Capsule/CapsuleTarget.swift b/Sources/ProHUD/Capsule/CapsuleTarget.swift index 85c8fc1..5030a85 100644 --- a/Sources/ProHUD/Capsule/CapsuleTarget.swift +++ b/Sources/ProHUD/Capsule/CapsuleTarget.swift @@ -11,11 +11,7 @@ open class CapsuleTarget: BaseController, HUDTargetType { public typealias ViewModel = CapsuleViewModel - @objc public lazy var config: CapsuleConfiguration = { - var cfg = CapsuleConfiguration() - CapsuleConfiguration.customGlobalConfig?(cfg) - return cfg - }() + @objc open lazy var config = CapsuleConfiguration() /// 内容容器(imageView、textLabel) public lazy var contentStack: StackView = { @@ -49,6 +45,9 @@ open class CapsuleTarget: BaseController, HUDTargetType { }() @objc public var vm: CapsuleViewModel? { + willSet { + vm?.cancelTimer() + } didSet { vm?.vc = self } @@ -64,7 +63,7 @@ open class CapsuleTarget: BaseController, HUDTargetType { } } - private var tapActionCallback: ((_ capsule: CapsuleTarget) -> Void)? + var tapActionCallback: ((_ capsule: CapsuleTarget) -> Void)? required public override init() { super.init() @@ -79,7 +78,7 @@ open class CapsuleTarget: BaseController, HUDTargetType { self.vm = vm } - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() view.layer.shadowRadius = 8 view.layer.shadowOffset = .init(width: 0, height: 5) diff --git a/Sources/ProHUD/Capsule/CapsuleWindow.swift b/Sources/ProHUD/Capsule/CapsuleWindow.swift index 2079b63..ab0c20b 100644 --- a/Sources/ProHUD/Capsule/CapsuleWindow.swift +++ b/Sources/ProHUD/Capsule/CapsuleWindow.swift @@ -14,6 +14,7 @@ class CapsuleWindow: Window { init(capsule: CapsuleTarget) { self.capsule = capsule super.init(frame: .zero) + isUserInteractionEnabled = false windowScene = AppContext.windowScene switch capsule.vm?.position { case .top, .none: diff --git a/Sources/ProHUD/Core/Controllers/BaseController.swift b/Sources/ProHUD/Core/Controllers/BaseController.swift index e1bcea0..fb974c9 100644 --- a/Sources/ProHUD/Core/Controllers/BaseController.swift +++ b/Sources/ProHUD/Core/Controllers/BaseController.swift @@ -25,7 +25,7 @@ open class BaseController: UIViewController { open var customView: UIView? - public internal(set) var isViewDisplayed = false + public internal(set) var isViewAppeared = false /// 按钮事件 var buttonEvents = [UIView: () -> Void]() @@ -43,7 +43,7 @@ open class BaseController: UIViewController { consolePrint("👌", self, "deinit") } - override public func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. @@ -89,7 +89,7 @@ open class BaseController: UIViewController { open override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - isViewDisplayed = true + isViewAppeared = true } } diff --git a/Sources/ProHUD/Core/Models/BaseViewModel.swift b/Sources/ProHUD/Core/Models/BaseViewModel.swift index ab32a33..3ef2dc9 100644 --- a/Sources/ProHUD/Core/Models/BaseViewModel.swift +++ b/Sources/ProHUD/Core/Models/BaseViewModel.swift @@ -48,11 +48,7 @@ open class BaseViewModel: NSObject, HUDViewModelType { @objc open var tintColor: UIColor? /// 持续时间(为空代表根据场景不同的默认配置,为0代表无穷大) - open var duration: TimeInterval? { - didSet { - resetTimeoutHandler() - } - } + open var duration: TimeInterval? weak var vc: BaseController? { didSet { @@ -72,25 +68,32 @@ open class BaseViewModel: NSObject, HUDViewModelType { self.duration = duration } - /// 超时处理 - var timeoutHandler: DispatchWorkItem? { - didSet { - resetTimeoutHandler() + private var timeoutTimer: Timer? + + func restartTimer() { + cancelTimer() + if let duration = duration { + if duration > 0 { + let timer = Timer(timeInterval: duration, repeats: false, block: { [weak self] t in + if let vc = self?.vc as? any HUDTargetType { + vc.pop() + } + }) + RunLoop.main.add(timer, forMode: .common) + timeoutTimer = timer + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { + if let vc = self.vc as? any HUDTargetType { + vc.pop() + } + } + } } } - var timeoutTimer: Timer? - - func resetTimeoutHandler() { + func cancelTimer() { timeoutTimer?.invalidate() timeoutTimer = nil - if let t = duration, t > 0 { - let timer = Timer(timeInterval: t, repeats: false, block: { [weak self] t in - self?.timeoutHandler?.perform() - }) - RunLoop.main.add(timer, forMode: .common) - timeoutTimer = timer - } } } @@ -103,6 +106,11 @@ public extension BaseViewModel { return self } + func lazyIdentifier(file: String = #file, line: Int = #line) -> Self { + self.identifier = (file + "#\(line)") + return self + } + func icon(_ image: UIImage?) -> Self { self.icon = image return self @@ -143,11 +151,18 @@ public extension BaseViewModel { // MARK: - example scenes public extension BaseViewModel { + /// 设置指定的唯一标识符 + /// - Parameter text: 唯一标识符 static func identifier(_ text: String?) -> Self { .init() .identifier(text) } + /// 以当前代码位置作为唯一标识符 + static func codeIdentifier(file: String = #file, line: Int = #line) -> Self { + identifier((file + "#\(line)")) + } + // MARK: plain static func title(_ text: String?) -> Self { .init() diff --git a/Sources/ProHUD/Core/Models/Configuration.swift b/Sources/ProHUD/Core/Models/Configuration.swift index 655bd04..0bdec18 100644 --- a/Sources/ProHUD/Core/Models/Configuration.swift +++ b/Sources/ProHUD/Core/Models/Configuration.swift @@ -188,7 +188,7 @@ open class CommonConfiguration: NSObject { customContentViewMask = callback } - override init() { + public override init() { } diff --git a/Sources/ProHUD/Core/Protocols/Provider.swift b/Sources/ProHUD/Core/Protocols/Provider.swift index 4eb8f9f..3b8bf07 100644 --- a/Sources/ProHUD/Core/Protocols/Provider.swift +++ b/Sources/ProHUD/Core/Protocols/Provider.swift @@ -18,21 +18,3 @@ public protocol HUDProviderType { } -open class HUDProvider: NSObject, HUDProviderType { - - /// 根据自定义的初始化代码创建一个Target并显示 - /// - Parameter initializer: 初始化代码(传空值时不会做任何事) - @discardableResult public required init(initializer: ((_ target: Target) -> Void)?) { - guard let initializer = initializer else { - // Provider的作用就是push一个target - // 如果没有任何初始化代码就没有target,就是个无意义的Provider - // 但为了支持lazyPush(找到已有实例并更新),所以就需要支持无意义的Provider - // 详见子类中的 self.init(initializer: nil) - return - } - var t = Target() - initializer(t) - t.push() - } - -} diff --git a/Sources/ProHUD/Core/Protocols/Target.swift b/Sources/ProHUD/Core/Protocols/Target.swift index db3ec52..3635f99 100644 --- a/Sources/ProHUD/Core/Protocols/Target.swift +++ b/Sources/ProHUD/Core/Protocols/Target.swift @@ -14,6 +14,7 @@ import UIKit public protocol HUDTargetType: HUDControllerType { associatedtype ViewModel = HUDViewModelType + var identifier: String { get set } var vm: ViewModel? { get set } init() } diff --git a/Sources/ProHUD/Core/Utils/RotateAnimation.swift b/Sources/ProHUD/Core/Utils/RotateAnimation.swift index 4ad6693..7ed636d 100644 --- a/Sources/ProHUD/Core/Utils/RotateAnimation.swift +++ b/Sources/ProHUD/Core/Utils/RotateAnimation.swift @@ -12,7 +12,7 @@ extension LoadingAnimation { /// 更新进度(如果需要显示进度,需要先调用一次 updateProgress(0) 来初始化) /// - Parameter progress: 进度(0~1) public func update(progress: CGFloat) { - guard isViewDisplayed else { return } + guard isViewAppeared else { return } guard let superview = imageView.superview else { return } if progressView == nil { let width = imageView.frame.size.width + ProgressView.lineWidth * 2 diff --git a/Sources/ProHUD/Core/Utils/ViewExts.swift b/Sources/ProHUD/Core/Utils/ViewExts.swift index e61fb9f..3563a4b 100644 --- a/Sources/ProHUD/Core/Utils/ViewExts.swift +++ b/Sources/ProHUD/Core/Utils/ViewExts.swift @@ -12,8 +12,8 @@ extension UIView { static func animateLinear(duration: TimeInterval, delay: TimeInterval = 0, animations: @escaping () -> Void, completion: ((_ done: Bool) -> Void)? = nil) { animate(withDuration: duration, delay: delay, options: [.allowUserInteraction], animations: animations, completion: completion) } - static func animateEaseIn(duration: TimeInterval, animations: @escaping () -> Void, completion: ((_ done: Bool) -> Void)? = nil) { - animate(withDuration: duration, delay: 0, options: [.allowUserInteraction, .curveEaseIn], animations: animations, completion: completion) + static func animateEaseIn(duration: TimeInterval, delay: TimeInterval = 0, animations: @escaping () -> Void, completion: ((_ done: Bool) -> Void)? = nil) { + animate(withDuration: duration, delay: delay, options: [.allowUserInteraction, .curveEaseIn], animations: animations, completion: completion) } static func animateEaseOut(duration: TimeInterval, animations: @escaping () -> Void, completion: ((_ done: Bool) -> Void)? = nil) { animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0.5, options: [.allowUserInteraction, .curveEaseOut], animations: animations, completion: completion) diff --git a/Sources/ProHUD/Sheet/SheetConfiguration.swift b/Sources/ProHUD/Sheet/SheetConfiguration.swift index 0e7b4dd..260ad44 100644 --- a/Sources/ProHUD/Sheet/SheetConfiguration.swift +++ b/Sources/ProHUD/Sheet/SheetConfiguration.swift @@ -26,7 +26,12 @@ public class SheetConfiguration: CommonConfiguration { customSubtitleLabel = handler } - static var customGlobalConfig: ((_ config: SheetConfiguration) -> Void)? + private static var customGlobalConfig: ((_ config: SheetConfiguration) -> Void)? + + public override init() { + super.init() + Self.customGlobalConfig?(self) + } /// 全局共享配置(只能设置一次,影响所有实例) /// - Parameter callback: 配置代码 diff --git a/Sources/ProHUD/Sheet/SheetConvenienceLayout.swift b/Sources/ProHUD/Sheet/SheetConvenienceLayout.swift index 7f9c434..4e63041 100644 --- a/Sources/ProHUD/Sheet/SheetConvenienceLayout.swift +++ b/Sources/ProHUD/Sheet/SheetConvenienceLayout.swift @@ -28,7 +28,7 @@ extension SheetTarget: ConvenienceLayout { self?.pop() } } - if isViewDisplayed { + if isViewAppeared { self.contentStack.layoutIfNeeded() UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) { self.view.layoutIfNeeded() @@ -74,7 +74,7 @@ extension SheetTarget: ConvenienceLayout { buttonEvents[view] = nil } } - if isViewDisplayed { + if isViewAppeared { UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) { self.contentStack.layoutIfNeeded() self.view.layoutIfNeeded() diff --git a/Sources/ProHUD/Sheet/SheetDefaultLayout.swift b/Sources/ProHUD/Sheet/SheetDefaultLayout.swift index 8feafdb..4ebd73a 100644 --- a/Sources/ProHUD/Sheet/SheetDefaultLayout.swift +++ b/Sources/ProHUD/Sheet/SheetDefaultLayout.swift @@ -29,7 +29,7 @@ extension SheetTarget: DefaultLayout { // content loadContentViewIfNeeded() - if isViewDisplayed { + if isViewAppeared { UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) { self.view.layoutIfNeeded() } diff --git a/Sources/ProHUD/Sheet/SheetManager.swift b/Sources/ProHUD/Sheet/SheetManager.swift index 9fbfedd..7392e18 100644 --- a/Sources/ProHUD/Sheet/SheetManager.swift +++ b/Sources/ProHUD/Sheet/SheetManager.swift @@ -17,11 +17,13 @@ extension SheetTarget { if let w = windows.first(where: { $0.sheet == self }) { isNew = false window = w + window.sheet = self } else { window = SheetWindow(sheet: self) isNew = true } window.rootViewController = self + if windows.contains(window) == false { windows.append(window) setContextWindows(windows) diff --git a/Sources/ProHUD/Sheet/SheetProvider.swift b/Sources/ProHUD/Sheet/SheetProvider.swift index dd15e43..e201d8c 100644 --- a/Sources/ProHUD/Sheet/SheetProvider.swift +++ b/Sources/ProHUD/Sheet/SheetProvider.swift @@ -7,20 +7,31 @@ import UIKit -open class SheetProvider: HUDProvider { +public final class SheetProvider: HUDProviderType { public typealias ViewModel = SheetViewModel public typealias Target = SheetTarget - @discardableResult @objc public required init(initializer: ((_ sheet: Target) -> Void)?) { - super.init(initializer: initializer) + /// 根据自定义的初始化代码创建一个Target并显示 + /// - Parameter initializer: 初始化代码(传空值时不会做任何事) + @discardableResult public required init(initializer: ((_ target: Target) -> Void)?) { + guard let initializer = initializer else { + // Provider的作用就是push一个target + // 如果没有任何初始化代码就没有target,就是个无意义的Provider + // 但为了支持lazyPush(找到已有实例并更新),所以就需要支持无意义的Provider + // 详见子类中的 self.init(initializer: nil) + return + } + let t = Target() + initializer(t) + t.push() } /// 如果不存在就创建并弹出一个HUD实例,如果存在就更新实例 /// - Parameters: /// - identifier: 实例唯一标识符(如果为空,则以代码位置为唯一标识符) /// - handler: 实例创建代码 - public static func lazyPush(identifier: String? = nil, file: String = #file, line: Int = #line, handler: @escaping (_ sheet: SheetTarget) -> Void, onExists: ((_ sheet: SheetTarget) -> Void)? = nil) { + public static func lazyPush(identifier: String? = nil, file: String = #file, line: Int = #line, handler: @escaping (_ sheet: Target) -> Void, onExists: ((_ sheet: Target) -> Void)? = nil) { let id = identifier ?? (file + "#\(line)") if let vc = find(identifier: id).last { vc.update(handler: onExists ?? handler) @@ -35,7 +46,7 @@ open class SheetProvider: HUDProvider { /// 查找HUD实例 /// - Parameter identifier: 唯一标识符 /// - Returns: HUD实例 - @discardableResult public static func find(identifier: String, update handler: ((_ sheet: SheetTarget) -> Void)? = nil) -> [SheetTarget] { + @discardableResult public static func find(identifier: String, update handler: ((_ sheet: Target) -> Void)? = nil) -> [Target] { let arr = AppContext.sheetWindows.values.flatMap({ $0 }).compactMap({ $0.sheet }).filter({ $0.identifier == identifier }) if let handler = handler { arr.forEach({ $0.update(handler: handler) }) diff --git a/Sources/ProHUD/Sheet/SheetTarget.swift b/Sources/ProHUD/Sheet/SheetTarget.swift index 1e237da..861001d 100644 --- a/Sources/ProHUD/Sheet/SheetTarget.swift +++ b/Sources/ProHUD/Sheet/SheetTarget.swift @@ -11,11 +11,7 @@ open class SheetTarget: BaseController, HUDTargetType { weak var window: SheetWindow? - public lazy var config: SheetConfiguration = { - var cfg = SheetConfiguration() - SheetConfiguration.customGlobalConfig?(cfg) - return cfg - }() + public lazy var config = SheetConfiguration() public lazy var backgroundView: UIView = { let v = UIView() diff --git a/Sources/ProHUD/Toast/ToastConfiguration.swift b/Sources/ProHUD/Toast/ToastConfiguration.swift index 4bba30d..4499f0e 100644 --- a/Sources/ProHUD/Toast/ToastConfiguration.swift +++ b/Sources/ProHUD/Toast/ToastConfiguration.swift @@ -9,7 +9,12 @@ import UIKit public class ToastConfiguration: CommonConfiguration { - static var customGlobalConfig: ((_ config: ToastConfiguration) -> Void)? + private static var customGlobalConfig: ((_ config: ToastConfiguration) -> Void)? + + public override init() { + super.init() + Self.customGlobalConfig?(self) + } /// 全局共享配置(只能设置一次,影响所有实例) /// - Parameter callback: 配置代码 diff --git a/Sources/ProHUD/Toast/ToastConvenienceLayout.swift b/Sources/ProHUD/Toast/ToastConvenienceLayout.swift index 3e5c881..2e27924 100644 --- a/Sources/ProHUD/Toast/ToastConvenienceLayout.swift +++ b/Sources/ProHUD/Toast/ToastConvenienceLayout.swift @@ -51,7 +51,7 @@ extension ToastTarget: ConvenienceLayout { self?.pop() } } - if isViewDisplayed { + if isViewAppeared { self.actionStack.layoutIfNeeded() UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) { self.view.layoutIfNeeded() @@ -99,7 +99,7 @@ extension ToastTarget: ConvenienceLayout { buttonEvents[view] = nil } } - if isViewDisplayed { + if isViewAppeared { UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) { self.contentStack.layoutIfNeeded() self.view.layoutIfNeeded() diff --git a/Sources/ProHUD/Toast/ToastDefaultLayout.swift b/Sources/ProHUD/Toast/ToastDefaultLayout.swift index 706fd25..2abacaf 100644 --- a/Sources/ProHUD/Toast/ToastDefaultLayout.swift +++ b/Sources/ProHUD/Toast/ToastDefaultLayout.swift @@ -87,8 +87,10 @@ extension ToastTarget: DefaultLayout { bodyLabel.text = vm?.message view.layoutIfNeeded() - // 设置持续时间 - updateTimeoutDuration() + if isViewAppeared { + // 更新时间 + updateTimeoutDuration() + } setupImageView() diff --git a/Sources/ProHUD/Toast/ToastManager.swift b/Sources/ProHUD/Toast/ToastManager.swift index 3c02513..1bf4e7b 100644 --- a/Sources/ProHUD/Toast/ToastManager.swift +++ b/Sources/ProHUD/Toast/ToastManager.swift @@ -43,6 +43,7 @@ extension ToastTarget { if let w = windows.first(where: { $0.toast == self }) { isNew = false window = w + window.toast = self } else { window = ToastWindow(toast: self) isNew = true @@ -66,10 +67,10 @@ extension ToastTarget { setContextWindows(windows) } ToastWindow.updateToastWindowsLayout(windows: windows) - + // 为了更连贯,从进入动画开始时就开始计时 + updateTimeoutDuration() func completion() { self.navEvents[.onViewDidAppear]?(self) - self.updateTimeoutDuration() } if isNew { window.transform = .init(translationX: 0, y: -window.frame.maxY) @@ -126,9 +127,7 @@ extension ToastTarget { vm?.duration = config.defaultDuration } // 设置持续时间 - vm?.timeoutHandler = DispatchWorkItem(block: { [weak self] in - self?.pop() - }) + vm?.restartTimer() } } @@ -190,3 +189,18 @@ extension ToastWindow { updateToastWindowsLayout(windows: wins) } } + +public class ToastManager: NSObject { + + /// 查找HUD实例 + /// - Parameter identifier: 唯一标识符 + /// - Returns: HUD实例 + @discardableResult 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) }) + } + return arr + } + +} diff --git a/Sources/ProHUD/Toast/ToastProvider.swift b/Sources/ProHUD/Toast/ToastProvider.swift index b65ce18..9dcdea5 100644 --- a/Sources/ProHUD/Toast/ToastProvider.swift +++ b/Sources/ProHUD/Toast/ToastProvider.swift @@ -7,13 +7,24 @@ import UIKit -open class ToastProvider: HUDProvider { +public final class ToastProvider: HUDProviderType { public typealias ViewModel = ToastViewModel public typealias Target = ToastTarget - @discardableResult @objc public required init(initializer: ((_ toast: Target) -> Void)?) { - super.init(initializer: initializer) + /// 根据自定义的初始化代码创建一个Target并显示 + /// - Parameter initializer: 初始化代码(传空值时不会做任何事) + @discardableResult public required init(initializer: ((_ target: Target) -> Void)?) { + guard let initializer = initializer else { + // Provider的作用就是push一个target + // 如果没有任何初始化代码就没有target,就是个无意义的Provider + // 但为了支持lazyPush(找到已有实例并更新),所以就需要支持无意义的Provider + // 详见子类中的 self.init(initializer: nil) + return + } + let t = Target() + initializer(t) + t.push() } /// 根据ViewModel和自定义的初始化代码创建一个Target并显示 @@ -21,10 +32,10 @@ open class ToastProvider: HUDProvider { /// - vm: 数据模型 /// - initializer: 自定义的初始化代码 @discardableResult public convenience init(_ vm: ViewModel, initializer: ((_ toast: Target) -> Void)?) { - if let id = vm.identifier, id.count > 0 { - Self.lazyPush(identifier: id) { target in - target.vm = vm - initializer?(target) + if let id = vm.identifier, id.count > 0, let target = ToastManager.find(identifier: id).last { + target.update { t in + t.vm = vm + initializer?(t) } self.init(initializer: nil) } else { @@ -47,34 +58,6 @@ open class ToastProvider: HUDProvider { self.init(.message(text), initializer: nil) } - - /// 如果不存在就创建并弹出一个HUD实例,如果存在就更新实例 - /// - Parameters: - /// - identifier: 实例唯一标识符(如果为空,则以代码位置为唯一标识符) - /// - handler: 实例创建代码 - @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) - } else { - Self.init { toast in - toast.identifier = id - handler(toast) - } - } - } - - /// 查找HUD实例 - /// - Parameter identifier: 唯一标识符 - /// - Returns: HUD实例 - @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) }) - } - return arr - } - } public typealias Toast = ToastProvider diff --git a/Sources/ProHUD/Toast/ToastTarget.swift b/Sources/ProHUD/Toast/ToastTarget.swift index 2a230a3..9eede2d 100644 --- a/Sources/ProHUD/Toast/ToastTarget.swift +++ b/Sources/ProHUD/Toast/ToastTarget.swift @@ -13,11 +13,7 @@ open class ToastTarget: BaseController, HUDTargetType { weak var window: ToastWindow? - public lazy var config: ToastConfiguration = { - var cfg = ToastConfiguration() - ToastConfiguration.customGlobalConfig?(cfg) - return cfg - }() + public lazy var config = ToastConfiguration() public var progressView: ProgressView? @@ -149,7 +145,7 @@ fileprivate extension ToastTarget { /// 拖拽事件 /// - Parameter sender: 手势 @objc func _onPanGesture(_ sender: UIPanGestureRecognizer) { - vm?.timeoutTimer?.invalidate() + vm?.cancelTimer() let point = sender.translation(in: sender.view) window?.transform = .init(translationX: 0, y: point.y) if sender.state == .recognized { @@ -164,8 +160,7 @@ fileprivate extension ToastTarget { UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) { self.window?.transform = .identity } completion: { done in - let d = self.vm?.duration - self.vm?.duration = d + self.vm?.restartTimer() } } }