mirror of https://github.com/xaoxuu/ProHUD
支持倒计时
This commit is contained in:
parent
4072478569
commit
a636c0d6b4
|
@ -47,9 +47,9 @@ class DemoAlertVC: ListVC {
|
||||||
section.add(title: "图标 + 文字") {
|
section.add(title: "图标 + 文字") {
|
||||||
Alert(.loading.message("正在加载")) { alert in
|
Alert(.loading.message("正在加载")) { alert in
|
||||||
updateProgress(in: 4) { percent in
|
updateProgress(in: 4) { percent in
|
||||||
alert.update(progress: percent)
|
alert.vm?.progress(percent)
|
||||||
} completion: {
|
} completion: {
|
||||||
alert.update { alert in
|
alert.reloadData { alert in
|
||||||
alert.vm = .success.message("加载成功")
|
alert.vm = .success.message("加载成功")
|
||||||
alert.add(action: "OK")
|
alert.add(action: "OK")
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,14 +59,33 @@ class DemoCapsuleVC: ListVC {
|
||||||
section.add(title: "下载进度") {
|
section.add(title: "下载进度") {
|
||||||
let capsule = CapsuleTarget()
|
let capsule = CapsuleTarget()
|
||||||
capsule.vm = .loading(.infinity).message("正在下载")
|
capsule.vm = .loading(.infinity).message("正在下载")
|
||||||
capsule.update(progress: 0)
|
|
||||||
capsule.push()
|
capsule.push()
|
||||||
updateProgress(in: 4) { percent in
|
updateProgress(in: 4) { percent in
|
||||||
capsule.update(progress: percent)
|
capsule.vm?.progress(percent)
|
||||||
} completion: {
|
} completion: {
|
||||||
capsule.update { toast in
|
capsule.vm(.success(5).message("下载成功"))
|
||||||
toast.vm = .success(5).message("下载成功")
|
}
|
||||||
}
|
}
|
||||||
|
section.add(title: "倒计时3s") {
|
||||||
|
Capsule(.icon(.init(named: "twemoji")).title("倒计时3s").duration(4)) { capsule in
|
||||||
|
capsule.config.cardMinWidth = 140 // 估算一下合适的宽度并固定,不然数字变化时总宽度也在一直变化,观感不佳
|
||||||
|
capsule.vm?.countdown(seconds: 3, onUpdate: { progress in
|
||||||
|
capsule.title = .init(format: "倒计时%.1fs", progress.current)
|
||||||
|
}, onCompletion: {
|
||||||
|
// 重新创建一个新的vm
|
||||||
|
capsule.vm(.success(3).title("倒计时结束"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
section.add(title: "倒计时10s") {
|
||||||
|
Capsule(.icon(.init(named: "twemoji")).title("倒计时3s").duration(10)) { capsule in
|
||||||
|
capsule.config.cardMinWidth = 140 // 估算一下合适的宽度并固定,不然数字变化时总宽度也在一直变化,观感不佳
|
||||||
|
capsule.vm?.countdown(seconds: 10, onUpdate: { progress in
|
||||||
|
capsule.title = .init(format: "倒计时%.1fs", progress.current)
|
||||||
|
}, onCompletion: {
|
||||||
|
// 重新创建一个新的vm
|
||||||
|
capsule.vm(.success(3).title("倒计时结束"))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
section.add(title: "接口报错提示") {
|
section.add(title: "接口报错提示") {
|
||||||
|
@ -283,7 +302,7 @@ class GradientCapsule: HUDProviderType {
|
||||||
/// - initializer: 初始化代码
|
/// - initializer: 初始化代码
|
||||||
@discardableResult public convenience init(_ vm: ViewModel, initializer: ((_ capsule: Target) -> Void)?) {
|
@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 {
|
if let id = vm.identifier, id.count > 0, let target = CapsuleManager.find(identifier: id).last as? Target {
|
||||||
target.update { capsule in
|
target.reloadData { capsule in
|
||||||
capsule.vm = vm
|
capsule.vm = vm
|
||||||
initializer?(capsule as! GradientCapsule.Target)
|
initializer?(capsule as! GradientCapsule.Target)
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,25 +128,23 @@ class DemoToastVC: ListVC {
|
||||||
let s2 = "这通常不会太久"
|
let s2 = "这通常不会太久"
|
||||||
let toast = ToastTarget(.loading.title(s1).message(s2))
|
let toast = ToastTarget(.loading.title(s1).message(s2))
|
||||||
toast.push()
|
toast.push()
|
||||||
toast.update(progress: 0)
|
|
||||||
updateProgress(in: 4) { percent in
|
updateProgress(in: 4) { percent in
|
||||||
toast.update(progress: percent)
|
toast.vm?.progress(percent)
|
||||||
} completion: {
|
} completion: {
|
||||||
toast.update { toast in
|
toast.vm(
|
||||||
toast.vm = .success(5)
|
.success(5)
|
||||||
.title("加载成功")
|
.title("加载成功")
|
||||||
.message("这条通知5s后消失")
|
.message("这条通知5s后消失")
|
||||||
.icon(.init(named: "twemoji"))
|
.icon(.init(named: "twemoji"))
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
section.add(title: "倒计时") {
|
section.add(title: "倒计时") {
|
||||||
let s1 = "笑容正在消失"
|
let s1 = "笑容正在消失"
|
||||||
let s2 = "这通常不会太久"
|
let s2 = "这通常不会太久"
|
||||||
Toast { toast in
|
Toast(.title(s1).message(s2).icon(UIImage(named: "twemoji"))) { toast in
|
||||||
toast.vm = .title(s1).message(s2).icon(UIImage(named: "twemoji"))
|
|
||||||
updateProgress(in: 5) { percent in
|
updateProgress(in: 5) { percent in
|
||||||
toast.update(progress: 1 - percent)
|
toast.vm?.progress(1 - percent)
|
||||||
} completion: {
|
} completion: {
|
||||||
toast.pop()
|
toast.pop()
|
||||||
}
|
}
|
||||||
|
|
|
@ -131,13 +131,6 @@ extension AlertTarget: DefaultLayout {
|
||||||
extension AlertTarget {
|
extension AlertTarget {
|
||||||
|
|
||||||
func setupImageView() {
|
func setupImageView() {
|
||||||
// 移除动画
|
|
||||||
stopRotate(animateLayer)
|
|
||||||
animateLayer = nil
|
|
||||||
animation = nil
|
|
||||||
|
|
||||||
// 移除进度
|
|
||||||
progressView?.removeFromSuperview()
|
|
||||||
|
|
||||||
if vm?.icon != nil || vm?.iconURL != nil {
|
if vm?.icon != nil || vm?.iconURL != nil {
|
||||||
imageView.image = vm?.icon
|
imageView.image = vm?.icon
|
||||||
|
@ -154,9 +147,6 @@ extension AlertTarget {
|
||||||
mk.height.equalTo(config.iconSizeByDefault.height)
|
mk.height.equalTo(config.iconSizeByDefault.height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let rotation = vm?.rotation {
|
|
||||||
startRotate(rotation)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if contentStack.arrangedSubviews.contains(imageView) {
|
if contentStack.arrangedSubviews.contains(imageView) {
|
||||||
contentStack.removeArrangedSubview(imageView)
|
contentStack.removeArrangedSubview(imageView)
|
||||||
|
@ -164,6 +154,9 @@ extension AlertTarget {
|
||||||
imageView.removeFromSuperview()
|
imageView.removeFromSuperview()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vm?.updateRotation()
|
||||||
|
vm?.updateProgress()
|
||||||
|
|
||||||
}
|
}
|
||||||
func setupTextStack() {
|
func setupTextStack() {
|
||||||
let titleCount = vm?.title?.count ?? 0
|
let titleCount = vm?.title?.count ?? 0
|
||||||
|
|
|
@ -65,14 +65,26 @@ extension AlertTarget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新HUD实例
|
/// 更新VC
|
||||||
/// - Parameter callback: 实例更新代码
|
/// - Parameter handler: 更新操作
|
||||||
@objc open func update(handler: @escaping (_ alert: AlertTarget) -> Void) {
|
@objc open func reloadData(handler: @escaping (_ capsule: AlertTarget) -> Void) {
|
||||||
handler(self)
|
handler(self)
|
||||||
reloadData()
|
reloadData()
|
||||||
UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) {
|
}
|
||||||
self.view.layoutIfNeeded()
|
|
||||||
}
|
/// 更新vm并刷新UI
|
||||||
|
/// - Parameter handler: 更新操作
|
||||||
|
@objc open func vm(handler: @escaping (_ vm: ViewModel) -> ViewModel) {
|
||||||
|
let new = handler(vm ?? .init())
|
||||||
|
vm?.update(another: new)
|
||||||
|
reloadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重设vm并刷新UI
|
||||||
|
/// - Parameter vm: 新的vm
|
||||||
|
@objc open func vm(_ vm: ViewModel) {
|
||||||
|
self.vm = vm
|
||||||
|
reloadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTimeoutDuration() {
|
func updateTimeoutDuration() {
|
||||||
|
@ -133,7 +145,7 @@ public class AlertManager: NSObject {
|
||||||
@discardableResult public static func find(identifier: String, update handler: ((_ alert: AlertTarget) -> Void)? = nil) -> [AlertTarget] {
|
@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 })
|
let arr = AppContext.alertWindow.values.flatMap({ $0.alerts }).filter({ $0.identifier == identifier })
|
||||||
if let handler = handler {
|
if let handler = handler {
|
||||||
arr.forEach({ $0.update(handler: handler) })
|
arr.forEach({ $0.reloadData(handler: handler) })
|
||||||
}
|
}
|
||||||
return arr
|
return arr
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ open class AlertProvider: HUDProviderType {
|
||||||
|
|
||||||
/// 根据自定义的初始化代码创建一个Target并显示
|
/// 根据自定义的初始化代码创建一个Target并显示
|
||||||
/// - Parameter initializer: 初始化代码(传空值时不会做任何事)
|
/// - Parameter initializer: 初始化代码(传空值时不会做任何事)
|
||||||
@discardableResult public required init(initializer: ((_ target: Target) -> Void)?) {
|
@discardableResult public required init(initializer: ((_ alert: Target) -> Void)?) {
|
||||||
guard let initializer = initializer else {
|
guard let initializer = initializer else {
|
||||||
// Provider的作用就是push一个target
|
// Provider的作用就是push一个target
|
||||||
// 如果没有任何初始化代码就没有target,就是个无意义的Provider
|
// 如果没有任何初始化代码就没有target,就是个无意义的Provider
|
||||||
|
@ -33,7 +33,7 @@ open class AlertProvider: HUDProviderType {
|
||||||
/// - initializer: 自定义的初始化代码
|
/// - initializer: 自定义的初始化代码
|
||||||
@discardableResult public convenience init(_ vm: ViewModel, initializer: ((_ alert: Target) -> Void)?) {
|
@discardableResult public convenience init(_ vm: ViewModel, initializer: ((_ alert: Target) -> Void)?) {
|
||||||
if let id = vm.identifier, id.count > 0, let target = AlertManager.find(identifier: id).last {
|
if let id = vm.identifier, id.count > 0, let target = AlertManager.find(identifier: id).last {
|
||||||
target.update { t in
|
target.reloadData { t in
|
||||||
t.vm = vm
|
t.vm = vm
|
||||||
initializer?(t)
|
initializer?(t)
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,7 @@ open class AlertTarget: BaseController, HUDTargetType {
|
||||||
didSet {
|
didSet {
|
||||||
if let vm = vm {
|
if let vm = vm {
|
||||||
vm.title = title
|
vm.title = title
|
||||||
|
titleLabel.text = title
|
||||||
} else {
|
} else {
|
||||||
vm = .title(title)
|
vm = .title(title)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,9 @@ public class CapsuleConfiguration: CommonConfiguration {
|
||||||
|
|
||||||
override var cardMaxHeightByDefault: CGFloat { cardMaxHeight ?? 120 }
|
override var cardMaxHeightByDefault: CGFloat { cardMaxHeight ?? 120 }
|
||||||
|
|
||||||
|
/// 最小宽度(当设置了最小宽度而内容没有达到时,内容布局默认靠左)
|
||||||
|
public var cardMinWidth: CGFloat? = nil
|
||||||
|
|
||||||
/// 最小高度
|
/// 最小高度
|
||||||
public var cardMinHeight = CGFloat(40)
|
public var cardMinHeight = CGFloat(40)
|
||||||
|
|
||||||
|
|
|
@ -74,20 +74,18 @@ extension CapsuleTarget: DefaultLayout {
|
||||||
if contentStack.superview == nil {
|
if contentStack.superview == nil {
|
||||||
view.addSubview(contentStack)
|
view.addSubview(contentStack)
|
||||||
contentStack.snp.remakeConstraints { make in
|
contentStack.snp.remakeConstraints { make in
|
||||||
make.center.equalToSuperview()
|
make.centerY.equalToSuperview()
|
||||||
|
if config.cardMinWidth != nil {
|
||||||
|
make.left.greaterThanOrEqualToSuperview().inset(config.cardEdgeInsetsByDefault.left)
|
||||||
|
} else {
|
||||||
|
make.centerX.equalToSuperview()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupImageView() {
|
private func setupImageView() {
|
||||||
// 移除动画
|
|
||||||
stopRotate(animateLayer)
|
|
||||||
animateLayer = nil
|
|
||||||
animation = nil
|
|
||||||
|
|
||||||
// 移除进度
|
|
||||||
progressView?.removeFromSuperview()
|
|
||||||
|
|
||||||
if vm?.icon == nil && vm?.iconURL == nil {
|
if vm?.icon == nil && vm?.iconURL == nil {
|
||||||
contentStack.removeArrangedSubview(imageView)
|
contentStack.removeArrangedSubview(imageView)
|
||||||
|
@ -106,9 +104,9 @@ extension CapsuleTarget: DefaultLayout {
|
||||||
if let iconURL = vm?.iconURL {
|
if let iconURL = vm?.iconURL {
|
||||||
config.customWebImage?(imageView, iconURL)
|
config.customWebImage?(imageView, iconURL)
|
||||||
}
|
}
|
||||||
if let rotation = vm?.rotation {
|
|
||||||
startRotate(rotation)
|
vm?.updateRotation()
|
||||||
}
|
vm?.updateProgress()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +116,7 @@ extension CapsuleTarget: DefaultLayout {
|
||||||
var size = contentStack.frame.size
|
var size = contentStack.frame.size
|
||||||
let width = min(config.cardMaxWidthByDefault, size.width + cardEdgeInsetsByDefault.left + cardEdgeInsetsByDefault.right)
|
let width = min(config.cardMaxWidthByDefault, size.width + cardEdgeInsetsByDefault.left + cardEdgeInsetsByDefault.right)
|
||||||
let height = min(config.cardMaxHeightByDefault, size.height + cardEdgeInsetsByDefault.top + cardEdgeInsetsByDefault.bottom)
|
let height = min(config.cardMaxHeightByDefault, size.height + cardEdgeInsetsByDefault.top + cardEdgeInsetsByDefault.bottom)
|
||||||
return .init(width: width, height: max(height, config.cardMinHeight))
|
return .init(width: max(width, config.cardMinWidth ?? 0), height: max(height, config.cardMinHeight))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getWindowFrame(size: CGSize) -> CGRect {
|
func getWindowFrame(size: CGSize) -> CGRect {
|
||||||
|
|
|
@ -172,15 +172,26 @@ extension CapsuleTarget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新HUD实例
|
/// 更新VC
|
||||||
/// - Parameter handler: 实例更新代码
|
/// - Parameter handler: 更新操作
|
||||||
@objc open func update(handler: @escaping (_ capsule: CapsuleTarget) -> Void) {
|
@objc open func reloadData(handler: @escaping (_ capsule: CapsuleTarget) -> Void) {
|
||||||
handler(self)
|
handler(self)
|
||||||
|
|
||||||
reloadData()
|
reloadData()
|
||||||
UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) {
|
}
|
||||||
self.view.layoutIfNeeded()
|
|
||||||
}
|
/// 更新vm并刷新UI
|
||||||
|
/// - Parameter handler: 更新操作
|
||||||
|
@objc open func vm(handler: @escaping (_ vm: ViewModel) -> ViewModel) {
|
||||||
|
let new = handler(vm ?? .init())
|
||||||
|
vm?.update(another: new)
|
||||||
|
reloadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重设vm并刷新UI
|
||||||
|
/// - Parameter vm: 新的vm
|
||||||
|
@objc open func vm(_ vm: ViewModel) {
|
||||||
|
self.vm = vm
|
||||||
|
reloadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTimeoutDuration() {
|
func updateTimeoutDuration() {
|
||||||
|
@ -204,7 +215,22 @@ public class CapsuleManager: NSObject {
|
||||||
let allCapsules = allPositions.compactMap({ $0.capsule })
|
let allCapsules = allPositions.compactMap({ $0.capsule })
|
||||||
let arr = (allCapsules + AppContext.capsuleInQueue).filter({ $0.identifier == identifier || $0.vm?.identifier == identifier })
|
let arr = (allCapsules + AppContext.capsuleInQueue).filter({ $0.identifier == identifier || $0.vm?.identifier == identifier })
|
||||||
if let handler = handler {
|
if let handler = handler {
|
||||||
arr.forEach({ $0.update(handler: handler) })
|
arr.forEach({ $0.reloadData(handler: handler) })
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查找HUD实例
|
||||||
|
/// - Parameters:
|
||||||
|
/// - position: 位置
|
||||||
|
/// - handler: 更新
|
||||||
|
/// - Returns: HUD实例
|
||||||
|
@discardableResult public static func find(position: CapsuleViewModel.Position, 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.vm?.position == position })
|
||||||
|
if let handler = handler {
|
||||||
|
arr.forEach({ $0.reloadData(handler: handler) })
|
||||||
}
|
}
|
||||||
return arr
|
return arr
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ open class CapsuleProvider: HUDProviderType {
|
||||||
|
|
||||||
/// 根据自定义的初始化代码创建一个Target并显示
|
/// 根据自定义的初始化代码创建一个Target并显示
|
||||||
/// - Parameter initializer: 初始化代码(传空值时不会做任何事)
|
/// - Parameter initializer: 初始化代码(传空值时不会做任何事)
|
||||||
@discardableResult public required init(initializer: ((_ target: Target) -> Void)?) {
|
@discardableResult public required init(initializer: ((_ capsule: Target) -> Void)?) {
|
||||||
guard let initializer = initializer else {
|
guard let initializer = initializer else {
|
||||||
// Provider的作用就是push一个target
|
// Provider的作用就是push一个target
|
||||||
// 如果没有任何初始化代码就没有target,就是个无意义的Provider
|
// 如果没有任何初始化代码就没有target,就是个无意义的Provider
|
||||||
|
@ -33,7 +33,7 @@ open class CapsuleProvider: HUDProviderType {
|
||||||
/// - initializer: 初始化代码
|
/// - initializer: 初始化代码
|
||||||
@discardableResult public convenience init(_ vm: ViewModel, initializer: ((_ capsule: Target) -> Void)?) {
|
@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 {
|
if let id = vm.identifier, id.count > 0, let target = CapsuleManager.find(identifier: id).last {
|
||||||
target.update { t in
|
target.reloadData { t in
|
||||||
t.vm = vm
|
t.vm = vm
|
||||||
initializer?(t)
|
initializer?(t)
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,7 @@ open class CapsuleTarget: BaseController, HUDTargetType {
|
||||||
didSet {
|
didSet {
|
||||||
if let vm = vm {
|
if let vm = vm {
|
||||||
vm.title = title
|
vm.title = title
|
||||||
|
textLabel.text = title
|
||||||
} else {
|
} else {
|
||||||
vm = .title(title)
|
vm = .title(title)
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,13 @@ open class BaseViewModel: NSObject, HUDViewModelType {
|
||||||
/// 持续时间(为空代表根据场景不同的默认配置,为0代表无穷大)
|
/// 持续时间(为空代表根据场景不同的默认配置,为0代表无穷大)
|
||||||
open var duration: TimeInterval?
|
open var duration: TimeInterval?
|
||||||
|
|
||||||
|
/// 进度条,大于0时显示,取值区间: 0~1
|
||||||
|
@objc open var progress: TimeProgress? {
|
||||||
|
didSet {
|
||||||
|
updateProgress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
weak var vc: BaseController? {
|
weak var vc: BaseController? {
|
||||||
didSet {
|
didSet {
|
||||||
if let id = tmpStoredIdentifier {
|
if let id = tmpStoredIdentifier {
|
||||||
|
@ -96,6 +103,108 @@ open class BaseViewModel: NSObject, HUDViewModelType {
|
||||||
timeoutTimer = nil
|
timeoutTimer = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc open func update(another vm: BaseViewModel) {
|
||||||
|
self.title(vm.title)
|
||||||
|
.message(vm.message)
|
||||||
|
.icon(vm.icon)
|
||||||
|
.icon(vm.iconURL)
|
||||||
|
.duration(vm.duration)
|
||||||
|
.rotation(vm.rotation)
|
||||||
|
.tintColor(vm.tintColor)
|
||||||
|
.progress(vm.progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BaseViewModel {
|
||||||
|
|
||||||
|
// MARK: rotation
|
||||||
|
|
||||||
|
func updateRotation() {
|
||||||
|
guard let vc = vc as? LoadingAnimation else { return }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let rotation = self.rotation {
|
||||||
|
vc.startRotate(rotation)
|
||||||
|
} else {
|
||||||
|
vc.stopRotate(vc.animateLayer)
|
||||||
|
vc.animateLayer = nil
|
||||||
|
vc.animation = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: progress
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
public func progress(_ progress: TimeProgress?) -> Self {
|
||||||
|
self.progress = progress
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
public func progress(_ newPercent: CGFloat) -> Self {
|
||||||
|
if progress == nil {
|
||||||
|
let p: TimeProgress = .init(total: 1)
|
||||||
|
p.set(newPercent: newPercent)
|
||||||
|
self.progress = p
|
||||||
|
} else {
|
||||||
|
self.progress?.set(newPercent: newPercent)
|
||||||
|
self.updateProgress()
|
||||||
|
}
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateProgress() {
|
||||||
|
guard let vc = vc as? LoadingAnimation else { return }
|
||||||
|
guard let superview = vc.imageView.superview else { return }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let progress = self.progress, progress.percent > 0 {
|
||||||
|
if vc.progressView == nil {
|
||||||
|
let width = vc.imageView.frame.size.width + ProgressView.lineWidth * 2
|
||||||
|
let v = ProgressView(frame: .init(origin: .zero, size: .init(width: width, height: width)))
|
||||||
|
superview.addSubview(v)
|
||||||
|
v.tintColor = superview.tintColor
|
||||||
|
v.snp.remakeConstraints { (mk) in
|
||||||
|
mk.center.equalTo(vc.imageView)
|
||||||
|
mk.width.height.equalTo(width)
|
||||||
|
}
|
||||||
|
vc.progressView = v
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
vc.progressView?.removeFromSuperview()
|
||||||
|
vc.progressView = nil
|
||||||
|
}
|
||||||
|
if let v = vc.progressView, let progress = self.progress {
|
||||||
|
v.progress = progress.percent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: countdown
|
||||||
|
|
||||||
|
public func countdown(seconds: TimeInterval, onUpdate: ((_ progress: TimeProgress) -> Void)?, onCompletion: (() -> Void)?) {
|
||||||
|
guard let vc = vc as? LoadingAnimation else { return }
|
||||||
|
guard seconds > 0 else {
|
||||||
|
// stop countdown
|
||||||
|
self.progress = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let progress: TimeProgress = .init(total: seconds, direction: .counterclockwise, onUpdate: onUpdate, onCompletion: onCompletion)
|
||||||
|
self.progress = progress
|
||||||
|
countdownLoop(after: progress.interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
func countdownLoop(after: TimeInterval) {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + after) {
|
||||||
|
if let p = self.progress, p.isFinish == false {
|
||||||
|
self.progress?.next()
|
||||||
|
self.updateProgress()
|
||||||
|
self.countdownLoop(after: p.interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - convenience func
|
// MARK: - convenience func
|
||||||
|
|
|
@ -10,20 +10,13 @@ import Foundation
|
||||||
public struct Rotation {
|
public struct Rotation {
|
||||||
|
|
||||||
/// 旋转方向
|
/// 旋转方向
|
||||||
public enum Direction: Double {
|
public var direction: TimeDirection = .clockwise
|
||||||
/// 顺时针
|
|
||||||
case clockwise = 1
|
|
||||||
/// 逆时针
|
|
||||||
case counterclockwise = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
public var direction: Direction = .clockwise
|
|
||||||
|
|
||||||
public var speed: CFTimeInterval = 2
|
public var speed: CFTimeInterval = 2
|
||||||
|
|
||||||
public var repeatCount: Float = .infinity
|
public var repeatCount: Float = .infinity
|
||||||
|
|
||||||
public init(direction: Direction = .clockwise, speed: CFTimeInterval = 2, repeatCount: Float = .infinity) {
|
public init(direction: TimeDirection = .clockwise, speed: CFTimeInterval = 2, repeatCount: Float = .infinity) {
|
||||||
self.direction = direction
|
self.direction = direction
|
||||||
self.speed = speed
|
self.speed = speed
|
||||||
self.repeatCount = repeatCount
|
self.repeatCount = repeatCount
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
//
|
||||||
|
// TimeDirection.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by xaoxuu on 2023/8/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum TimeDirection: Double {
|
||||||
|
/// 顺时针
|
||||||
|
case clockwise = 1
|
||||||
|
/// 逆时针
|
||||||
|
case counterclockwise = -1
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
//
|
||||||
|
// TimeProgress.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by xaoxuu on 2023/8/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public class TimeProgress: NSObject {
|
||||||
|
|
||||||
|
public var total: TimeInterval
|
||||||
|
|
||||||
|
public var current: TimeInterval
|
||||||
|
|
||||||
|
// 每秒10跳
|
||||||
|
var interval: TimeInterval = 0.1
|
||||||
|
|
||||||
|
public var direction: TimeDirection
|
||||||
|
|
||||||
|
public var percent: CGFloat {
|
||||||
|
guard total > 0 else { return 0 }
|
||||||
|
return current / total
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isFinish: Bool {
|
||||||
|
switch direction {
|
||||||
|
case .clockwise:
|
||||||
|
return current >= total
|
||||||
|
case .counterclockwise:
|
||||||
|
return current <= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(total: TimeInterval, direction: TimeDirection = .clockwise, onUpdate: ((_ progress: TimeProgress) -> Void)? = nil, onCompletion: (() -> Void)? = nil) {
|
||||||
|
self.total = total
|
||||||
|
self.direction = direction
|
||||||
|
switch direction {
|
||||||
|
case .clockwise:
|
||||||
|
// 顺时针从0开始
|
||||||
|
self.current = 0
|
||||||
|
case .counterclockwise:
|
||||||
|
// 逆时针(倒计时)从total开始
|
||||||
|
self.current = total
|
||||||
|
}
|
||||||
|
self.onUpdate = onUpdate
|
||||||
|
self.onCompletion = onCompletion
|
||||||
|
}
|
||||||
|
|
||||||
|
func next() {
|
||||||
|
switch direction {
|
||||||
|
case .clockwise:
|
||||||
|
current = min(total, current + interval)
|
||||||
|
case .counterclockwise:
|
||||||
|
current = max(0, current - interval)
|
||||||
|
}
|
||||||
|
onUpdate?(self)
|
||||||
|
if isFinish {
|
||||||
|
onCompletion?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func set(newPercent: CGFloat) {
|
||||||
|
current = total * newPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
var onUpdate: ((_ progress: TimeProgress) -> Void)?
|
||||||
|
var onCompletion: (() -> Void)?
|
||||||
|
|
||||||
|
}
|
|
@ -13,8 +13,4 @@ public protocol LoadingAnimation: BaseController {
|
||||||
var imageView: UIImageView { get }
|
var imageView: UIImageView { get }
|
||||||
var progressView: ProgressView? { get set }
|
var progressView: ProgressView? { get set }
|
||||||
|
|
||||||
/// 更新进度
|
|
||||||
/// - Parameter progress: 进度百分比(0~1)
|
|
||||||
func update(progress: CGFloat)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,27 +9,6 @@ import UIKit
|
||||||
|
|
||||||
extension LoadingAnimation {
|
extension LoadingAnimation {
|
||||||
|
|
||||||
/// 更新进度(如果需要显示进度,需要先调用一次 updateProgress(0) 来初始化)
|
|
||||||
/// - Parameter progress: 进度(0~1)
|
|
||||||
public func update(progress: CGFloat) {
|
|
||||||
guard isViewAppeared else { return }
|
|
||||||
guard let superview = imageView.superview else { return }
|
|
||||||
if progressView == nil {
|
|
||||||
let width = imageView.frame.size.width + ProgressView.lineWidth * 2
|
|
||||||
let v = ProgressView(frame: .init(origin: .zero, size: .init(width: width, height: width)))
|
|
||||||
superview.addSubview(v)
|
|
||||||
v.tintColor = superview.tintColor
|
|
||||||
v.snp.remakeConstraints { (mk) in
|
|
||||||
mk.center.equalTo(imageView)
|
|
||||||
mk.width.height.equalTo(width)
|
|
||||||
}
|
|
||||||
progressView = v
|
|
||||||
}
|
|
||||||
if let v = progressView {
|
|
||||||
v.updateProgress(progress: progress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 旋转动画
|
/// 旋转动画
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - layer: 图层
|
/// - layer: 图层
|
||||||
|
|
|
@ -10,6 +10,12 @@ import UIKit
|
||||||
/// 进度指示器
|
/// 进度指示器
|
||||||
public class ProgressView: UIView {
|
public class ProgressView: UIView {
|
||||||
|
|
||||||
|
var progress: CGFloat = 0 {
|
||||||
|
didSet {
|
||||||
|
updateProgress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var progressLayer = CAShapeLayer()
|
var progressLayer = CAShapeLayer()
|
||||||
|
|
||||||
static var lineWidth: CGFloat { 4 }
|
static var lineWidth: CGFloat { 4 }
|
||||||
|
@ -57,12 +63,16 @@ public class ProgressView: UIView {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateProgress(progress: CGFloat) {
|
func updateProgress() {
|
||||||
if progressLayer.superlayer == nil {
|
if progress > 0 {
|
||||||
progressLayer.strokeEnd = 0
|
if progressLayer.superlayer == nil {
|
||||||
layer.addSublayer(progressLayer)
|
progressLayer.strokeEnd = 0
|
||||||
|
layer.addSublayer(progressLayer)
|
||||||
|
}
|
||||||
|
progressLayer.strokeEnd = max(min(progress, 1), 0)
|
||||||
|
} else {
|
||||||
|
progressLayer.removeFromSuperlayer()
|
||||||
}
|
}
|
||||||
progressLayer.strokeEnd = max(min(progress, 1), 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,21 +138,14 @@ extension ToastTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupImageView() {
|
func setupImageView() {
|
||||||
// 移除动画
|
|
||||||
stopRotate(animateLayer)
|
|
||||||
animateLayer = nil
|
|
||||||
animation = nil
|
|
||||||
|
|
||||||
// 移除进度
|
|
||||||
progressView?.removeFromSuperview()
|
|
||||||
|
|
||||||
imageView.image = vm?.icon
|
imageView.image = vm?.icon
|
||||||
if let iconURL = vm?.iconURL {
|
if let iconURL = vm?.iconURL {
|
||||||
config.customWebImage?(imageView, iconURL)
|
config.customWebImage?(imageView, iconURL)
|
||||||
}
|
}
|
||||||
if let rotation = vm?.rotation {
|
|
||||||
startRotate(rotation)
|
vm?.updateRotation()
|
||||||
}
|
vm?.updateProgress()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -76,14 +76,26 @@ extension ToastTarget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新HUD实例
|
/// 更新VC
|
||||||
/// - Parameter handler: 实例更新代码
|
/// - Parameter handler: 更新操作
|
||||||
@objc open func update(handler: @escaping (_ toast: ToastTarget) -> Void) {
|
@objc open func reloadData(handler: @escaping (_ capsule: ToastTarget) -> Void) {
|
||||||
handler(self)
|
handler(self)
|
||||||
reloadData()
|
reloadData()
|
||||||
UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) {
|
}
|
||||||
self.view.layoutIfNeeded()
|
|
||||||
}
|
/// 更新vm并刷新UI
|
||||||
|
/// - Parameter handler: 更新操作
|
||||||
|
@objc open func vm(handler: @escaping (_ vm: ViewModel) -> ViewModel) {
|
||||||
|
let new = handler(vm ?? .init())
|
||||||
|
vm?.update(another: new)
|
||||||
|
reloadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重设vm并刷新UI
|
||||||
|
/// - Parameter vm: 新的vm
|
||||||
|
@objc open func vm(_ vm: ViewModel) {
|
||||||
|
self.vm = vm
|
||||||
|
reloadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTimeoutDuration() {
|
func updateTimeoutDuration() {
|
||||||
|
@ -163,7 +175,7 @@ public class ToastManager: NSObject {
|
||||||
@discardableResult public static func find(identifier: String, update handler: ((_ toast: ToastTarget) -> Void)? = nil) -> [ToastTarget] {
|
@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 })
|
let arr = AppContext.toastWindows.values.flatMap({ $0 }).compactMap({ $0.toast }).filter({ $0.identifier == identifier })
|
||||||
if let handler = handler {
|
if let handler = handler {
|
||||||
arr.forEach({ $0.update(handler: handler) })
|
arr.forEach({ $0.reloadData(handler: handler) })
|
||||||
}
|
}
|
||||||
return arr
|
return arr
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ open class ToastProvider: HUDProviderType {
|
||||||
|
|
||||||
/// 根据自定义的初始化代码创建一个Target并显示
|
/// 根据自定义的初始化代码创建一个Target并显示
|
||||||
/// - Parameter initializer: 初始化代码(传空值时不会做任何事)
|
/// - Parameter initializer: 初始化代码(传空值时不会做任何事)
|
||||||
@discardableResult public required init(initializer: ((_ target: Target) -> Void)?) {
|
@discardableResult public required init(initializer: ((_ toast: Target) -> Void)?) {
|
||||||
guard let initializer = initializer else {
|
guard let initializer = initializer else {
|
||||||
// Provider的作用就是push一个target
|
// Provider的作用就是push一个target
|
||||||
// 如果没有任何初始化代码就没有target,就是个无意义的Provider
|
// 如果没有任何初始化代码就没有target,就是个无意义的Provider
|
||||||
|
@ -33,7 +33,7 @@ open class ToastProvider: HUDProviderType {
|
||||||
/// - initializer: 自定义的初始化代码
|
/// - initializer: 自定义的初始化代码
|
||||||
@discardableResult public convenience init(_ vm: ViewModel, initializer: ((_ toast: Target) -> Void)?) {
|
@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 {
|
if let id = vm.identifier, id.count > 0, let target = ToastManager.find(identifier: id).last {
|
||||||
target.update { t in
|
target.reloadData { t in
|
||||||
t.vm = vm
|
t.vm = vm
|
||||||
initializer?(t)
|
initializer?(t)
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,6 +94,7 @@ open class ToastTarget: BaseController, HUDTargetType {
|
||||||
didSet {
|
didSet {
|
||||||
if let vm = vm {
|
if let vm = vm {
|
||||||
vm.title = title
|
vm.title = title
|
||||||
|
titleLabel.text = title
|
||||||
} else {
|
} else {
|
||||||
vm = .title(title)
|
vm = .title(title)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue