支持倒计时

This commit is contained in:
xaoxuu 2023-08-25 16:28:52 +08:00
parent 4072478569
commit a636c0d6b4
23 changed files with 347 additions and 118 deletions

View File

@ -47,9 +47,9 @@ class DemoAlertVC: ListVC {
section.add(title: "图标 + 文字") {
Alert(.loading.message("正在加载")) { alert in
updateProgress(in: 4) { percent in
alert.update(progress: percent)
alert.vm?.progress(percent)
} completion: {
alert.update { alert in
alert.reloadData { alert in
alert.vm = .success.message("加载成功")
alert.add(action: "OK")
}

View File

@ -59,15 +59,34 @@ class DemoCapsuleVC: ListVC {
section.add(title: "下载进度") {
let capsule = CapsuleTarget()
capsule.vm = .loading(.infinity).message("正在下载")
capsule.update(progress: 0)
capsule.push()
updateProgress(in: 4) { percent in
capsule.update(progress: percent)
capsule.vm?.progress(percent)
} completion: {
capsule.update { toast in
toast.vm = .success(5).message("下载成功")
capsule.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: "接口报错提示") {
Capsule(.systemError.title("[500]").message("服务端错误"))
@ -283,7 +302,7 @@ class GradientCapsule: HUDProviderType {
/// - 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
target.reloadData { capsule in
capsule.vm = vm
initializer?(capsule as! GradientCapsule.Target)
}

View File

@ -128,25 +128,23 @@ class DemoToastVC: ListVC {
let s2 = "这通常不会太久"
let toast = ToastTarget(.loading.title(s1).message(s2))
toast.push()
toast.update(progress: 0)
updateProgress(in: 4) { percent in
toast.update(progress: percent)
toast.vm?.progress(percent)
} completion: {
toast.update { toast in
toast.vm = .success(5)
toast.vm(
.success(5)
.title("加载成功")
.message("这条通知5s后消失")
.icon(.init(named: "twemoji"))
}
)
}
}
section.add(title: "倒计时") {
let s1 = "笑容正在消失"
let s2 = "这通常不会太久"
Toast { toast in
toast.vm = .title(s1).message(s2).icon(UIImage(named: "twemoji"))
Toast(.title(s1).message(s2).icon(UIImage(named: "twemoji"))) { toast in
updateProgress(in: 5) { percent in
toast.update(progress: 1 - percent)
toast.vm?.progress(1 - percent)
} completion: {
toast.pop()
}

View File

@ -131,13 +131,6 @@ extension AlertTarget: DefaultLayout {
extension AlertTarget {
func setupImageView() {
//
stopRotate(animateLayer)
animateLayer = nil
animation = nil
//
progressView?.removeFromSuperview()
if vm?.icon != nil || vm?.iconURL != nil {
imageView.image = vm?.icon
@ -154,9 +147,6 @@ extension AlertTarget {
mk.height.equalTo(config.iconSizeByDefault.height)
}
}
if let rotation = vm?.rotation {
startRotate(rotation)
}
} else {
if contentStack.arrangedSubviews.contains(imageView) {
contentStack.removeArrangedSubview(imageView)
@ -164,6 +154,9 @@ extension AlertTarget {
imageView.removeFromSuperview()
}
vm?.updateRotation()
vm?.updateProgress()
}
func setupTextStack() {
let titleCount = vm?.title?.count ?? 0

View File

@ -65,14 +65,26 @@ extension AlertTarget {
}
}
/// HUD
/// - Parameter callback:
@objc open func update(handler: @escaping (_ alert: AlertTarget) -> Void) {
/// VC
/// - Parameter handler:
@objc open func reloadData(handler: @escaping (_ capsule: AlertTarget) -> Void) {
handler(self)
reloadData()
UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) {
self.view.layoutIfNeeded()
}
/// vmUI
/// - Parameter handler:
@objc open func vm(handler: @escaping (_ vm: ViewModel) -> ViewModel) {
let new = handler(vm ?? .init())
vm?.update(another: new)
reloadData()
}
/// vmUI
/// - Parameter vm: vm
@objc open func vm(_ vm: ViewModel) {
self.vm = vm
reloadData()
}
func updateTimeoutDuration() {
@ -133,7 +145,7 @@ public class AlertManager: NSObject {
@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) })
arr.forEach({ $0.reloadData(handler: handler) })
}
return arr
}

View File

@ -14,7 +14,7 @@ open class AlertProvider: HUDProviderType {
/// Target
/// - Parameter initializer:
@discardableResult public required init(initializer: ((_ target: Target) -> Void)?) {
@discardableResult public required init(initializer: ((_ alert: Target) -> Void)?) {
guard let initializer = initializer else {
// Providerpushtarget
// targetProvider
@ -33,7 +33,7 @@ open class AlertProvider: HUDProviderType {
/// - initializer:
@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 {
target.update { t in
target.reloadData { t in
t.vm = vm
initializer?(t)
}

View File

@ -82,6 +82,7 @@ open class AlertTarget: BaseController, HUDTargetType {
didSet {
if let vm = vm {
vm.title = title
titleLabel.text = title
} else {
vm = .title(title)
}

View File

@ -31,6 +31,9 @@ public class CapsuleConfiguration: CommonConfiguration {
override var cardMaxHeightByDefault: CGFloat { cardMaxHeight ?? 120 }
/// ()
public var cardMinWidth: CGFloat? = nil
///
public var cardMinHeight = CGFloat(40)

View File

@ -74,20 +74,18 @@ extension CapsuleTarget: DefaultLayout {
if contentStack.superview == nil {
view.addSubview(contentStack)
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() {
//
stopRotate(animateLayer)
animateLayer = nil
animation = nil
//
progressView?.removeFromSuperview()
if vm?.icon == nil && vm?.iconURL == nil {
contentStack.removeArrangedSubview(imageView)
@ -106,9 +104,9 @@ extension CapsuleTarget: DefaultLayout {
if let iconURL = vm?.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
let width = min(config.cardMaxWidthByDefault, size.width + cardEdgeInsetsByDefault.left + cardEdgeInsetsByDefault.right)
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 {

View File

@ -172,15 +172,26 @@ extension CapsuleTarget {
}
}
/// HUD
/// - Parameter handler:
@objc open func update(handler: @escaping (_ capsule: CapsuleTarget) -> Void) {
/// VC
/// - Parameter handler:
@objc open func reloadData(handler: @escaping (_ capsule: CapsuleTarget) -> Void) {
handler(self)
reloadData()
UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) {
self.view.layoutIfNeeded()
}
/// vmUI
/// - Parameter handler:
@objc open func vm(handler: @escaping (_ vm: ViewModel) -> ViewModel) {
let new = handler(vm ?? .init())
vm?.update(another: new)
reloadData()
}
/// vmUI
/// - Parameter vm: vm
@objc open func vm(_ vm: ViewModel) {
self.vm = vm
reloadData()
}
func updateTimeoutDuration() {
@ -204,7 +215,22 @@ public class CapsuleManager: NSObject {
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) })
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
}

View File

@ -14,7 +14,7 @@ open class CapsuleProvider: HUDProviderType {
/// Target
/// - Parameter initializer:
@discardableResult public required init(initializer: ((_ target: Target) -> Void)?) {
@discardableResult public required init(initializer: ((_ capsule: Target) -> Void)?) {
guard let initializer = initializer else {
// Providerpushtarget
// targetProvider
@ -33,7 +33,7 @@ open class CapsuleProvider: HUDProviderType {
/// - 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 {
target.update { t in
target.reloadData { t in
t.vm = vm
initializer?(t)
}

View File

@ -55,6 +55,7 @@ open class CapsuleTarget: BaseController, HUDTargetType {
didSet {
if let vm = vm {
vm.title = title
textLabel.text = title
} else {
vm = .title(title)
}

View File

@ -50,6 +50,13 @@ open class BaseViewModel: NSObject, HUDViewModelType {
/// 0
open var duration: TimeInterval?
/// 0: 0~1
@objc open var progress: TimeProgress? {
didSet {
updateProgress()
}
}
weak var vc: BaseController? {
didSet {
if let id = tmpStoredIdentifier {
@ -96,6 +103,108 @@ open class BaseViewModel: NSObject, HUDViewModelType {
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

View File

@ -10,20 +10,13 @@ import Foundation
public struct Rotation {
///
public enum Direction: Double {
///
case clockwise = 1
///
case counterclockwise = -1
}
public var direction: Direction = .clockwise
public var direction: TimeDirection = .clockwise
public var speed: CFTimeInterval = 2
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.speed = speed
self.repeatCount = repeatCount

View File

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

View File

@ -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)?
}

View File

@ -13,8 +13,4 @@ public protocol LoadingAnimation: BaseController {
var imageView: UIImageView { get }
var progressView: ProgressView? { get set }
///
/// - Parameter progress: 01
func update(progress: CGFloat)
}

View File

@ -9,27 +9,6 @@ import UIKit
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:
/// - layer:

View File

@ -10,6 +10,12 @@ import UIKit
///
public class ProgressView: UIView {
var progress: CGFloat = 0 {
didSet {
updateProgress()
}
}
var progressLayer = CAShapeLayer()
static var lineWidth: CGFloat { 4 }
@ -57,12 +63,16 @@ public class ProgressView: UIView {
fatalError("init(coder:) has not been implemented")
}
func updateProgress(progress: CGFloat) {
func updateProgress() {
if progress > 0 {
if progressLayer.superlayer == nil {
progressLayer.strokeEnd = 0
layer.addSublayer(progressLayer)
}
progressLayer.strokeEnd = max(min(progress, 1), 0)
} else {
progressLayer.removeFromSuperlayer()
}
}
}

View File

@ -138,21 +138,14 @@ extension ToastTarget {
}
func setupImageView() {
//
stopRotate(animateLayer)
animateLayer = nil
animation = nil
//
progressView?.removeFromSuperview()
imageView.image = vm?.icon
if let iconURL = vm?.iconURL {
config.customWebImage?(imageView, iconURL)
}
if let rotation = vm?.rotation {
startRotate(rotation)
}
vm?.updateRotation()
vm?.updateProgress()
}

View File

@ -76,14 +76,26 @@ extension ToastTarget {
}
}
/// HUD
/// - Parameter handler:
@objc open func update(handler: @escaping (_ toast: ToastTarget) -> Void) {
/// VC
/// - Parameter handler:
@objc open func reloadData(handler: @escaping (_ capsule: ToastTarget) -> Void) {
handler(self)
reloadData()
UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) {
self.view.layoutIfNeeded()
}
/// vmUI
/// - Parameter handler:
@objc open func vm(handler: @escaping (_ vm: ViewModel) -> ViewModel) {
let new = handler(vm ?? .init())
vm?.update(another: new)
reloadData()
}
/// vmUI
/// - Parameter vm: vm
@objc open func vm(_ vm: ViewModel) {
self.vm = vm
reloadData()
}
func updateTimeoutDuration() {
@ -163,7 +175,7 @@ public class ToastManager: NSObject {
@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) })
arr.forEach({ $0.reloadData(handler: handler) })
}
return arr
}

View File

@ -14,7 +14,7 @@ open class ToastProvider: HUDProviderType {
/// Target
/// - Parameter initializer:
@discardableResult public required init(initializer: ((_ target: Target) -> Void)?) {
@discardableResult public required init(initializer: ((_ toast: Target) -> Void)?) {
guard let initializer = initializer else {
// Providerpushtarget
// targetProvider
@ -33,7 +33,7 @@ open class ToastProvider: HUDProviderType {
/// - 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 {
target.update { t in
target.reloadData { t in
t.vm = vm
initializer?(t)
}

View File

@ -94,6 +94,7 @@ open class ToastTarget: BaseController, HUDTargetType {
didSet {
if let vm = vm {
vm.title = title
titleLabel.text = title
} else {
vm = .title(title)
}