代码重构

This commit is contained in:
xaoxuu 2023-08-19 14:10:59 +08:00
parent 32f9af1ae6
commit 0574844a7f
24 changed files with 272 additions and 173 deletions

View File

@ -29,14 +29,15 @@ class AlertVC: ListVC {
list.add(title: "纯文字") { section in list.add(title: "纯文字") { section in
section.add(title: "只有一句话") { section.add(title: "只有一句话") {
Alert(.message("只有一句话").duration(2)) // Alert(.message("").duration(2))
//
Alert("只有一句话")
} }
section.add(title: "标题 + 正文") { section.add(title: "标题 + 正文") {
let title = "这是标题" let title = "这是标题"
let message = "这是正文文字支持自动换行可设置最小宽度和最大宽度。这个弹窗将会持续4秒。" let message = "这是正文文字支持自动换行可设置最小宽度和最大宽度。这个弹窗将会持续4秒。"
Alert { alert in Alert { alert in
alert.vm = .title(title).message(message) alert.vm = .title(title).message(message).duration(4)
alert.vm.duration = 4
} }
} }
} }
@ -62,9 +63,9 @@ class AlertVC: ListVC {
} }
section.add(title: "图标 + 标题 + 正文") { section.add(title: "图标 + 标题 + 正文") {
Alert(.error) { alert in Alert(.error) { alert in
alert.vm.title = "加载失败" alert.vm?.title = "加载失败"
alert.vm.message = "请稍后重试" alert.vm?.message = "请稍后重试"
alert.vm.duration = 3 alert.vm?.duration = 3
} }
} }
} }
@ -81,7 +82,7 @@ class AlertVC: ListVC {
alert.config.customButton { button in alert.config.customButton { button in
button.titleLabel?.font = .systemFont(ofSize: 15) button.titleLabel?.font = .systemFont(ofSize: 15)
} }
alert.vm.title = "你正在使用移动网络观看" alert.title = "你正在使用移动网络观看"
alert.onViewDidLoad { vc in alert.onViewDidLoad { vc in
guard let alert = vc as? AlertTarget else { guard let alert = vc as? AlertTarget else {
return return
@ -111,7 +112,7 @@ class AlertVC: ListVC {
alert.config.customButton { button in alert.config.customButton { button in
button.titleLabel?.font = .systemFont(ofSize: 15) button.titleLabel?.font = .systemFont(ofSize: 15)
} }
alert.vm.message = "为了维护社区氛围,上麦用户需进行主播认证" alert.vm?.message = "为了维护社区氛围,上麦用户需进行主播认证"
alert.onViewDidLoad { vc in alert.onViewDidLoad { vc in
guard let alert = vc as? AlertTarget else { guard let alert = vc as? AlertTarget else {
return return
@ -141,7 +142,7 @@ class AlertVC: ListVC {
alert.config.customButton { button in alert.config.customButton { button in
button.titleLabel?.font = .systemFont(ofSize: 15) button.titleLabel?.font = .systemFont(ofSize: 15)
} }
alert.vm.message = "本次消费需要你支付999软妹豆确认支付吗" alert.vm?.message = "本次消费需要你支付999软妹豆确认支付吗"
alert.config.customActionStack { stack in alert.config.customActionStack { stack in
stack.spacing = 0 stack.spacing = 0
stack.axis = .vertical // stack.axis = .vertical //
@ -187,15 +188,15 @@ class AlertVC: ListVC {
section.add(title: "只有一段文字 + 按钮") { section.add(title: "只有一段文字 + 按钮") {
Alert { alert in Alert { alert in
alert.vm.title = "只有一段文字" alert.title = "只有一段文字"
alert.add(action: "取消", style: .gray) alert.add(action: "取消", style: .gray)
alert.add(action: "默认按钮") alert.add(action: "默认按钮")
} }
} }
section.add(title: "标题 + 正文 + 按钮") { section.add(title: "标题 + 正文 + 按钮") {
Alert { alert in Alert { alert in
alert.vm.title = "标题" alert.vm?.title = "标题"
alert.vm.message = "这是一段正文,长度超出最大宽度时会自动换行" alert.vm?.message = "这是一段正文,长度超出最大宽度时会自动换行"
alert.add(action: "取消", style: .gray) alert.add(action: "取消", style: .gray)
alert.add(action: "删除", style: .destructive) { alert in alert.add(action: "删除", style: .destructive) { alert in
// pop // pop
@ -229,8 +230,8 @@ class AlertVC: ListVC {
} }
section.add(title: "确认删除") { section.add(title: "确认删除") {
Alert(.delete) { alert in Alert(.delete) { alert in
alert.vm.title = "确认删除" alert.vm?.title = "确认删除"
alert.vm.message = "此操作无法撤销" alert.vm?.message = "此操作无法撤销"
alert.add(action: "取消", style: .gray) alert.add(action: "取消", style: .gray)
alert.add(action: "删除", style: .destructive) alert.add(action: "删除", style: .destructive)
} }
@ -239,7 +240,7 @@ class AlertVC: ListVC {
list.add(title: "控件管理") { section in list.add(title: "控件管理") { section in
section.add(title: "按钮增删改查") { section.add(title: "按钮增删改查") {
Alert(.note) { alert in Alert(.note) { alert in
alert.vm.message = "可以动态增加、删除按钮" alert.vm?.message = "可以动态增加、删除按钮"
alert.add(action: "在底部增加按钮", style: .filled(color: .systemGreen)) { alert in alert.add(action: "在底部增加按钮", style: .filled(color: .systemGreen)) { alert in
alert.add(action: "哈哈1", identifier: "haha1") alert.add(action: "哈哈1", identifier: "haha1")
} }
@ -265,36 +266,34 @@ class AlertVC: ListVC {
} }
} }
section.add(title: "更新文字") { section.add(title: "更新文字") {
Alert(.note) { alert in Alert(.note.message("可以动态增加、删除、更新文字")) { alert in
alert.vm.message = "可以动态增加、删除、更新文字"
alert.add(action: "增加标题") { alert in alert.add(action: "增加标题") { alert in
alert.vm.title = "这是标题" alert.vm?.title = "这是标题"
alert.reloadTextStack() alert.reloadTextStack()
} }
alert.add(action: "增加正文") { alert in alert.add(action: "增加正文") { alert in
alert.vm.message = "可以动态增加、删除、更新文字" alert.vm?.message = "可以动态增加、删除、更新文字"
alert.reloadTextStack() alert.reloadTextStack()
} }
alert.add(action: "删除标题", style: .destructive) { alert in alert.add(action: "删除标题", style: .destructive) { alert in
alert.vm.title = nil alert.vm?.title = nil
alert.reloadTextStack() alert.reloadTextStack()
} }
alert.add(action: "删除正文", style: .destructive) { alert in alert.add(action: "删除正文", style: .destructive) { alert in
alert.vm.message = nil alert.vm?.message = nil
alert.reloadTextStack() alert.reloadTextStack()
} }
alert.add(action: "取消", style: .gray) alert.add(action: "取消", style: .gray)
} }
} }
section.add(title: "在弹出过程中增加元素") { section.add(title: "在弹出过程中增加元素") {
Alert(.loading) { alert in Alert(.loading.title("在弹出过程中增加元素")) { alert in
alert.vm.title = "在弹出过程中增加元素"
alert.add(action: "OK", style: .gray) alert.add(action: "OK", style: .gray)
alert.onViewWillAppear { vc in alert.onViewWillAppear { vc in
guard let alert = vc as? AlertTarget else { guard let alert = vc as? AlertTarget else {
return return
} }
alert.vm.message = "这是一段后增加的文字\n动画效果会有细微差别" alert.vm?.message = "这是一段后增加的文字\n动画效果会有细微差别"
alert.reloadTextStack() alert.reloadTextStack()
} }
} }
@ -304,8 +303,8 @@ class AlertVC: ListVC {
section.add(title: "多层级弹窗") { section.add(title: "多层级弹窗") {
func f(i: Int) { func f(i: Int) {
Alert { alert in Alert { alert in
alert.vm.title = "\(i)次弹" alert.vm?.title = "\(i)次弹"
alert.vm.message = "每次都是一个新的实例覆盖在上一个弹窗上面,而背景不会叠加变深。" alert.vm?.message = "每次都是一个新的实例覆盖在上一个弹窗上面,而背景不会叠加变深。"
alert.add(action: "取消", style: .gray) alert.add(action: "取消", style: .gray)
alert.add(action: "增加一个") { alert in alert.add(action: "增加一个") { alert in
f(i: i + 1) f(i: i + 1)
@ -357,7 +356,7 @@ class AlertVC: ListVC {
list.add(title: "自定义视图") { section in list.add(title: "自定义视图") { section in
section.add(title: "自定义控件") { section.add(title: "自定义控件") {
Alert { alert in Alert { alert in
alert.vm.title = "自定义控件" alert.title = "自定义控件"
// //
let imgv = UIImageView(image: UIImage(named: "landscape")) let imgv = UIImageView(image: UIImage(named: "landscape"))
imgv.contentMode = .scaleAspectFill imgv.contentMode = .scaleAspectFill

View File

@ -17,12 +17,15 @@ class CapsuleVC: ListVC {
header.detailLabel.text = "状态胶囊控件,用于状态显示,一个主程序窗口每个位置(上中下)各自最多只有一个状态胶囊实例。" header.detailLabel.text = "状态胶囊控件,用于状态显示,一个主程序窗口每个位置(上中下)各自最多只有一个状态胶囊实例。"
CapsuleConfiguration.global { config in CapsuleConfiguration.global { config in
config.defaultDuration = 3 //
// config.cardCornerRadius = .infinity // // config.cardCornerRadius = .infinity //
} }
list.add(title: "默认布局:纯文字") { section in list.add(title: "默认布局:纯文字") { section in
section.add(title: "一条简短的消息") { section.add(title: "一条简短的消息") {
// vmhandlerpushvm // vmhandlerpushvm
Capsule(.message("一条简短消息")) // Capsule(.message(""))
//
Capsule("一条简短消息")
} }
section.add(title: "一条稍微长一点的消息") { section.add(title: "一条稍微长一点的消息") {
// vmhandlerpushhandler // vmhandlerpushhandler
@ -31,13 +34,15 @@ class CapsuleVC: ListVC {
capsule.vm = .message("一条稍微长一点的消息") capsule.vm = .message("一条稍微长一点的消息")
} }
} }
section.add(title: "(默认)状态胶囊控件,用于状态显示,一个主程序窗口只有一个状态胶囊实例。") { section.add(title: "延迟显示") {
// push // push
let obj = Capsule().target let obj = Capsule().target
obj.vm = .message("状态胶囊控件,用于状态显示,一个主程序窗口只有一个状态胶囊实例。") obj.vm = .message("状态胶囊控件,用于状态显示,一个主程序窗口只有一个状态胶囊实例。")
// ... push // ... push
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
obj.push() obj.push()
} }
}
section.add(title: "限制1行状态胶囊控件用于状态显示一个主程序窗口只有一个状态胶囊实例。") { section.add(title: "限制1行状态胶囊控件用于状态显示一个主程序窗口只有一个状态胶囊实例。") {
// vmhandler // vmhandler
Capsule(.message("状态胶囊控件,用于状态显示,一个主程序窗口只有一个状态胶囊实例。")) { capsule in Capsule(.message("状态胶囊控件,用于状态显示,一个主程序窗口只有一个状态胶囊实例。")) { capsule in
@ -146,7 +151,7 @@ class CapsuleVC: ListVC {
} }
extension CapsuleTarget.ViewModel { extension CapsuleViewModel {
static func info(_ text: String?) -> Self { static func info(_ text: String?) -> Self {
.init() .init()

View File

@ -25,7 +25,7 @@ class SheetVC: ListVC {
sheet.add(spacing: 24) sheet.add(spacing: 24)
sheet.add(action: "确认", style: .destructive) { sheet in sheet.add(action: "确认", style: .destructive) { sheet in
Alert(.confirm) { alert in Alert(.confirm) { alert in
alert.vm.title = "处理点击事件" alert.title = "处理点击事件"
alert.add(action: "我知道了") alert.add(action: "我知道了")
} }
} }
@ -74,12 +74,11 @@ class SheetVC: ListVC {
sheet.add(action: "确认") sheet.add(action: "确认")
sheet.add(action: "取消", style: .gray) sheet.add(action: "取消", style: .gray)
sheet.onTappedBackground { sheet in sheet.onTappedBackground { sheet in
print("点击了背景")
Toast.lazyPush(identifier: "alert") { toast in Toast.lazyPush(identifier: "alert") { toast in
toast.vm = .error toast.vm = .error
toast.vm.title = "点击了背景" .title("点击了背景")
toast.vm.message = "点击背景将不会dismiss必须在下方做出选择才能关掉" .message("点击背景将不会dismiss必须在下方做出选择才能关掉")
toast.vm.duration = 2 .duration(2)
} }
} }
} }

View File

@ -39,7 +39,10 @@ class TestToastTarget: ToastTarget {
} }
} }
typealias TestToast = HUDProvider<ToastViewModel, TestToastTarget> //typealias TestToast = HUDProvider<ToastViewModel, TestToastTarget>
class TestToast: ToastProvider {
typealias Target = TestToastTarget
}
class ToastVC: ListVC { class ToastVC: ListVC {
@ -54,6 +57,7 @@ class ToastVC: ListVC {
header.detailLabel.text = message header.detailLabel.text = message
ToastConfiguration.global { config in ToastConfiguration.global { config in
config.defaultDuration = 5
config.contentViewMask { mask in config.contentViewMask { mask in
mask.backgroundColor = .clear mask.backgroundColor = .clear
mask.effect = UIBlurEffect(style: .systemChromeMaterial) mask.effect = UIBlurEffect(style: .systemChromeMaterial)
@ -68,7 +72,9 @@ class ToastVC: ListVC {
TestToast(.title(title).message(message)) TestToast(.title(title).message(message))
} }
section.add(title: "一段长文本") { section.add(title: "一段长文本") {
Toast(.message(message)) // Toast(.message(message))
//
Toast(message)
} }
section.add(title: "图标 + 标题 + 正文") { section.add(title: "图标 + 标题 + 正文") {
let s1 = "笑容正在加载" let s1 = "笑容正在加载"
@ -80,8 +86,10 @@ class ToastVC: ListVC {
toast.update(progress: percent) toast.update(progress: percent)
} completion: { } completion: {
toast.update { toast in toast.update { toast in
toast.vm = .success(5).title("加载成功").message("这条通知5s后消失") toast.vm = .success(5)
toast.vm.icon = UIImage(named: "twemoji") .title("加载成功")
.message("这条通知5s后消失")
.icon(.init(named: "twemoji"))
} }
} }
} }
@ -119,16 +127,15 @@ class ToastVC: ListVC {
section.add(title: "增加按钮") { section.add(title: "增加按钮") {
let title = "您收到了一条好友申请" let title = "您收到了一条好友申请"
let message = "丹妮莉丝·坦格利安申请添加您为好友,是否同意?" let message = "丹妮莉丝·坦格利安申请添加您为好友,是否同意?"
Toast(.title(title).message(message)) { toast in Toast(.title(title).message(message).icon(.init(named: "avatar"))) { toast in
toast.isRemovable = false toast.isRemovable = false
toast.vm.icon = UIImage(named: "avatar")
toast.imageView.layer.masksToBounds = true toast.imageView.layer.masksToBounds = true
toast.imageView.layer.cornerRadius = toast.config.iconSize.width / 2 toast.imageView.layer.cornerRadius = toast.config.iconSize.width / 2
toast.add(action: "拒绝", style: .destructive) { toast in toast.add(action: "拒绝", style: .destructive) { toast in
Alert.lazyPush(identifier: "Dracarys") { alert in Alert.lazyPush(identifier: "Dracarys") { alert in
alert.vm = .message("Dracarys") alert.vm = .message("Dracarys")
alert.vm.icon = UIImage(inProHUD: "prohud.windmill") .icon(UIImage(inProHUD: "prohud.windmill"))
alert.vm.rotation = .init(repeatCount: .infinity) .rotation(.init(repeatCount: .infinity))
alert.config.enableShadow = false alert.config.enableShadow = false
alert.config.contentViewMask { mask in alert.config.contentViewMask { mask in
mask.effect = .none mask.effect = .none
@ -217,7 +224,7 @@ class ToastVC: ListVC {
} }
section.add(title: "修改左右外边距") { section.add(title: "修改左右外边距") {
Toast(.message("这条toast的左右外边距经过自定义设置与其它的有所不同。")) { toast in Toast(.message("这条toast的左右外边距经过自定义设置与其它的有所不同。")) { toast in
toast.config.windowEdgeInset = 8 toast.config.marginX = 32
toast.config.cardCornerRadius = 24 toast.config.cardCornerRadius = 24
} }
} }
@ -306,7 +313,7 @@ class ToastVC: ListVC {
section.add(title: "卡片背景样式") { section.add(title: "卡片背景样式") {
Toast { toast in Toast { toast in
toast.vm.title = "卡片背景样式" toast.title = "卡片背景样式"
toast.add(action: "浅色毛玻璃") { toast in toast.add(action: "浅色毛玻璃") { toast in
toast.contentMaskView.effect = UIBlurEffect(style: .light) toast.contentMaskView.effect = UIBlurEffect(style: .light)
toast.contentMaskView.backgroundColor = .clear toast.contentMaskView.backgroundColor = .clear
@ -327,7 +334,7 @@ class ToastVC: ListVC {
func foo() { func foo() {
Toast { toast in Toast { toast in
toast.title = "共享配置" toast.title = "共享配置"
toast.vm.message = "建议在App启动后进行通用配置设置所有实例都会先拉取通用配置为默认值修改这些配置会影响到所有实例。" toast.vm?.message = "建议在App启动后进行通用配置设置所有实例都会先拉取通用配置为默认值修改这些配置会影响到所有实例。"
toast.add(action: "默认", style: .gray) { toast in toast.add(action: "默认", style: .gray) { toast in
ToastConfiguration.global { config in ToastConfiguration.global { config in
config.customTitleLabel { titleLabel in config.customTitleLabel { titleLabel in
@ -359,7 +366,7 @@ class ToastVC: ListVC {
fileprivate func testAlert() { fileprivate func testAlert() {
Alert { alert in Alert { alert in
alert.vm.title = "处理点击事件" alert.title = "处理点击事件"
alert.add(action: "我知道了", style: .destructive) alert.add(action: "我知道了", style: .destructive)
} }
} }

View File

@ -17,7 +17,7 @@ extension AlertTarget: DefaultLayout {
if self.cfg.customReloadData?(self) == true { if self.cfg.customReloadData?(self) == true {
return return
} }
view.tintColor = vm.tintColor ?? config.tintColor view.tintColor = vm?.tintColor ?? config.tintColor
let isFirstLayout: Bool let isFirstLayout: Bool
if contentView.superview == nil { if contentView.superview == nil {
isFirstLayout = animated isFirstLayout = animated
@ -126,7 +126,7 @@ extension AlertTarget: DefaultLayout {
func updateTimeoutDuration() { func updateTimeoutDuration() {
// //
vm.timeoutHandler = DispatchWorkItem(block: { [weak self] in vm?.timeoutHandler = DispatchWorkItem(block: { [weak self] in
self?.pop() self?.pop()
}) })
} }
@ -144,9 +144,9 @@ extension AlertTarget {
// //
progressView?.removeFromSuperview() progressView?.removeFromSuperview()
if vm.icon != nil || vm.iconURL != nil { if vm?.icon != nil || vm?.iconURL != nil {
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 imageView.superview == nil { if imageView.superview == nil {
@ -159,7 +159,7 @@ extension AlertTarget {
mk.height.equalTo(config.iconSize.height) mk.height.equalTo(config.iconSize.height)
} }
} }
if let rotation = vm.rotation { if let rotation = vm?.rotation {
startRotate(rotation) startRotate(rotation)
} }
} else { } else {
@ -171,8 +171,8 @@ extension AlertTarget {
} }
func setupTextStack() { func setupTextStack() {
let titleCount = vm.title?.count ?? 0 let titleCount = vm?.title?.count ?? 0
let bodyCount = vm.message?.count ?? 0 let bodyCount = vm?.message?.count ?? 0
if titleCount > 0 || bodyCount > 0 { if titleCount > 0 || bodyCount > 0 {
if textStack.superview != contentStack { if textStack.superview != contentStack {
if let index = contentStack.arrangedSubviews.firstIndex(of: imageView) { if let index = contentStack.arrangedSubviews.firstIndex(of: imageView) {
@ -189,7 +189,7 @@ extension AlertTarget {
} }
} }
if titleCount > 0 { if titleCount > 0 {
titleLabel.text = vm.title titleLabel.text = vm?.title
if titleLabel.superview != textStack { if titleLabel.superview != textStack {
textStack.insertArrangedSubview(titleLabel, at: 0) textStack.insertArrangedSubview(titleLabel, at: 0)
} }
@ -209,7 +209,7 @@ extension AlertTarget {
titleLabel.removeFromSuperview() titleLabel.removeFromSuperview()
} }
if bodyCount > 0 { if bodyCount > 0 {
bodyLabel.text = vm.message bodyLabel.text = vm?.message
if bodyLabel.superview != textStack { if bodyLabel.superview != textStack {
textStack.addArrangedSubview(bodyLabel) textStack.addArrangedSubview(bodyLabel)
} }

View File

@ -8,13 +8,30 @@
import UIKit import UIKit
open class AlertProvider: HUDProvider<AlertViewModel, AlertTarget> { open class AlertProvider: HUDProvider<AlertViewModel, AlertTarget> {
@discardableResult public required init(_ vm: ViewModel?, initializer: ((_ alert: Target) -> Void)?) {
super.init(vm, initializer: initializer) public typealias ViewModel = AlertViewModel
public typealias Target = AlertTarget
@discardableResult @objc public required init(initializer: ((_ alert: Target) -> Void)?) {
super.init(initializer: initializer)
} }
@discardableResult public required convenience init(initializer: ((_ alert: Target) -> Void)?) { @discardableResult public convenience init(_ vm: ViewModel, initializer: ((_ alert: Target) -> Void)?) {
self.init(nil, initializer: initializer) self.init { alert in
alert.vm = vm
initializer?(alert)
} }
}
/// ViewModelTarget
/// - Parameter vm:
@discardableResult public convenience init(_ vm: ViewModel) {
self.init(vm, initializer: nil)
}
@discardableResult @objc public convenience init(_ text: String, duration: TimeInterval = 3) {
self.init(.message(text).duration(duration), initializer: nil)
}
/// HUD /// HUD
/// - Parameters: /// - Parameters:

View File

@ -74,11 +74,15 @@ open class AlertTarget: BaseController, HUDTargetType {
}() }()
/// ///
@objc public var vm: AlertViewModel = .init() @objc public var vm: AlertViewModel?
public override var title: String? { public override var title: String? {
didSet { didSet {
if let vm = vm {
vm.title = title vm.title = title
} else {
vm = .title(title)
}
} }
} }

View File

@ -19,6 +19,9 @@ public class CapsuleConfiguration: CommonConfiguration {
customGlobalConfig = callback customGlobalConfig = callback
} }
///
public var defaultDuration: TimeInterval = 3
override var cardCornerRadiusByDefault: CGFloat { override var cardCornerRadiusByDefault: CGFloat {
cardCornerRadius ?? 16 cardCornerRadius ?? 16
} }

View File

@ -18,7 +18,7 @@ extension CapsuleTarget: DefaultLayout {
return return
} }
view.tintColor = vm.tintColor ?? config.tintColor view.tintColor = vm?.tintColor ?? config.tintColor
// content // content
loadContentViewIfNeeded() loadContentViewIfNeeded()
@ -29,7 +29,7 @@ extension CapsuleTarget: DefaultLayout {
// text // text
textLabel.removeFromSuperview() textLabel.removeFromSuperview()
var text = [vm.title ?? "", vm.message ?? ""].filter({ $0.count > 0 }).joined(separator: " ") var text = [vm?.title ?? "", vm?.message ?? ""].filter({ $0.count > 0 }).joined(separator: " ")
if text.count > 0 { if text.count > 0 {
contentStack.addArrangedSubview(textLabel) contentStack.addArrangedSubview(textLabel)
textLabel.snp.makeConstraints { make in textLabel.snp.makeConstraints { make in
@ -81,11 +81,11 @@ extension CapsuleTarget: DefaultLayout {
private func updateTimeoutDuration() { private func updateTimeoutDuration() {
// 使 // 使
if vm.duration == nil { if vm?.duration == nil {
vm.duration = 3 vm?.duration = config.defaultDuration
} }
// //
vm.timeoutHandler = DispatchWorkItem(block: { [weak self] in vm?.timeoutHandler = DispatchWorkItem(block: { [weak self] in
self?.pop() self?.pop()
}) })
} }
@ -99,16 +99,16 @@ extension CapsuleTarget: DefaultLayout {
// //
progressView?.removeFromSuperview() progressView?.removeFromSuperview()
if vm.icon == nil && vm.iconURL == nil { if vm?.icon == nil && vm?.iconURL == nil {
contentStack.removeArrangedSubview(imageView) contentStack.removeArrangedSubview(imageView)
} else { } else {
contentStack.insertArrangedSubview(imageView, at: 0) contentStack.insertArrangedSubview(imageView, at: 0)
} }
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 { if let rotation = vm?.rotation {
startRotate(rotation) startRotate(rotation)
} }

View File

@ -13,7 +13,7 @@ extension CapsuleTarget {
guard CapsuleConfiguration.isEnabled else { return } guard CapsuleConfiguration.isEnabled else { return }
let isNew: Bool let isNew: Bool
let window: CapsuleWindow let window: CapsuleWindow
let position = vm.position let position = vm?.position ?? .top
if let w = AppContext.current?.capsuleWindows[position] { if let w = AppContext.current?.capsuleWindows[position] {
isNew = false isNew = false
@ -30,10 +30,10 @@ extension CapsuleTarget {
// frame // frame
let newFrame: CGRect let newFrame: CGRect
switch vm.position { switch vm?.position {
case .top: case .top, .none:
let topLayoutMargins = AppContext.appWindow?.layoutMargins.top ?? 8 let topLayoutMargins = AppContext.appWindow?.safeAreaInsets.top ?? 8
let y = max(topLayoutMargins - 8, 8) let y = max(topLayoutMargins, 8)
newFrame = .init(x: (AppContext.appBounds.width - size.width) / 2, y: y, width: size.width, height: size.height) newFrame = .init(x: (AppContext.appBounds.width - size.width) / 2, y: y, width: size.width, height: size.height)
case .middle: case .middle:
newFrame = .init(x: (AppContext.appBounds.width - size.width) / 2, y: (AppContext.appBounds.height - size.height) / 2 - 20, width: size.width, height: size.height) newFrame = .init(x: (AppContext.appBounds.width - size.width) / 2, y: (AppContext.appBounds.height - size.height) / 2 - 20, width: size.width, height: size.height)
@ -60,6 +60,10 @@ extension CapsuleTarget {
AppContext.capsuleWindows[s]?[position] = window AppContext.capsuleWindows[s]?[position] = window
} }
navEvents[.onViewWillAppear]?(self) navEvents[.onViewWillAppear]?(self)
// toast
ToastWindow.updateToastWindowsLayout()
if isNew { if isNew {
window.isHidden = false window.isHidden = false
func completion() { func completion() {
@ -116,8 +120,11 @@ extension CapsuleTarget {
@objc open func pop() { @objc open func pop() {
guard let window = attachedWindow, let windowScene = windowScene else { return } guard let window = attachedWindow, let windowScene = windowScene else { return }
AppContext.capsuleWindows[windowScene]?[vm.position] = nil AppContext.capsuleWindows[windowScene]?[vm?.position ?? .top] = nil
navEvents[.onViewWillDisappear]?(self) navEvents[.onViewWillDisappear]?(self)
// toast
ToastWindow.updateToastWindowsLayout()
func completion() { func completion() {
window.isHidden = true window.isHidden = true
window.transform = .identity window.transform = .identity
@ -128,8 +135,8 @@ extension CapsuleTarget {
} else { } else {
let duration = config.animateDurationForBuildOutByDefault let duration = config.animateDurationForBuildOutByDefault
let oldFrame = window.frame let oldFrame = window.frame
switch vm.position { switch vm?.position {
case .top: case .top, .none:
UIView.animateEaseOut(duration: duration) { UIView.animateEaseOut(duration: duration) {
window.transform = .init(translationX: 0, y: -oldFrame.maxY - 20) window.transform = .init(translationX: 0, y: -oldFrame.maxY - 20)
} completion: { done in } completion: { done in

View File

@ -7,20 +7,37 @@
import UIKit import UIKit
open class CapsuleProvider: HUDProvider<CapsuleTarget.ViewModel, CapsuleTarget> { open class CapsuleProvider: HUDProvider<CapsuleViewModel, CapsuleTarget> {
public typealias ViewModel = CapsuleViewModel
public typealias Target = CapsuleTarget
@discardableResult @objc public required init(initializer: ((_ capsule: Target) -> Void)?) {
super.init(initializer: initializer)
}
/// ViewModelTarget /// ViewModelTarget
/// - Parameters: /// - Parameters:
/// - vm: /// - vm:
/// - initializer: /// - initializer:
@discardableResult public required init(_ vm: ViewModel?, initializer: ((_ capsule: Target) -> Void)?) { @discardableResult public convenience init(_ vm: ViewModel, initializer: ((_ capsule: Target) -> Void)?) {
super.init(vm, initializer: initializer) self.init { capsule in
capsule.vm = vm
initializer?(capsule)
}
} }
@discardableResult public required convenience init(initializer: ((_ capsule: Target) -> Void)?) { /// ViewModelTarget
self.init(nil, initializer: initializer) /// - Parameter vm:
@discardableResult public convenience init(_ vm: ViewModel) {
self.init(vm, initializer: nil)
} }
@discardableResult public convenience init(_ text: String) {
self.init(.message(text), initializer: nil)
}
/// HUD /// HUD
/// - Parameters: /// - Parameters:
/// - identifier: /// - identifier:

View File

@ -46,7 +46,17 @@ open class CapsuleTarget: BaseController, HUDTargetType {
return lb return lb
}() }()
public var vm: CapsuleViewModel = .init() public var vm: CapsuleViewModel?
public override var title: String? {
didSet {
if let vm = vm {
vm.title = title
} else {
vm = .title(title)
}
}
}
private var tapActionCallback: ((_ capsule: CapsuleTarget) -> Void)? private var tapActionCallback: ((_ capsule: CapsuleTarget) -> Void)?

View File

@ -15,8 +15,8 @@ class CapsuleWindow: Window {
self.capsule = capsule self.capsule = capsule
super.init(frame: .zero) super.init(frame: .zero)
windowScene = AppContext.windowScene windowScene = AppContext.windowScene
switch capsule.vm.position { switch capsule.vm?.position {
case .top: case .top, .none:
// toast // toast
windowLevel = .phCapsuleTop windowLevel = .phCapsuleTop
case .middle: case .middle:

View File

@ -12,11 +12,9 @@ public protocol HUDProviderType {
associatedtype ViewModel = HUDViewModelType associatedtype ViewModel = HUDViewModelType
associatedtype Target = HUDTargetType associatedtype Target = HUDTargetType
/// ViewModelTarget /// Target
/// - Parameters: /// - Parameter initializer:
/// - vm: @discardableResult init(initializer: ((_ target: Target) -> Void)?)
/// - initializer:
@discardableResult init(_ vm: ViewModel?, initializer: ((_ target: Target) -> Void)?)
} }
@ -25,39 +23,22 @@ open class HUDProvider<ViewModel: HUDViewModelType, Target: HUDTargetType>: HUDP
/// HUD /// HUD
public var target: Target public var target: Target
/// ViewModelTarget /// Target
/// - Parameters: /// - Parameter initializer:
/// - vm: @discardableResult public required init(initializer: ((_ target: Target) -> Void)?) {
/// - initializer:
@discardableResult public required init(_ vm: ViewModel?, initializer: ((_ target: Target) -> Void)?) {
var t = Target() var t = Target()
if let vm = vm as? Target.ViewModel {
t.vm = vm
}
initializer?(t) initializer?(t)
self.target = t self.target = t
if (vm == nil && initializer == nil) == false { if (t.vm == nil && initializer == nil) == false {
DispatchQueue.main.async { DispatchQueue.main.async {
t.push() t.push()
} }
} }
} }
/// Target
/// - Parameter initializer:
@discardableResult public convenience init(initializer: ((_ target: Target) -> Void)?) {
self.init(nil, initializer: initializer)
}
/// ViewModelTarget
/// - Parameter vm:
@discardableResult public convenience init(_ vm: ViewModel?) {
self.init(vm, initializer: nil)
}
/// target.push() /// target.push()
@discardableResult public convenience init() { @discardableResult public convenience init() {
self.init(nil, initializer: nil) self.init(initializer: nil)
} }

View File

@ -14,6 +14,6 @@ import UIKit
public protocol HUDTargetType: HUDControllerType { public protocol HUDTargetType: HUDControllerType {
associatedtype ViewModel = HUDViewModelType associatedtype ViewModel = HUDViewModelType
var vm: ViewModel { get set } var vm: ViewModel? { get set }
init() init()
} }

View File

@ -36,7 +36,7 @@ public struct AppContext {
static var toastWindows: [UIWindowScene: [ToastWindow]] = [:] static var toastWindows: [UIWindowScene: [ToastWindow]] = [:]
static var alertWindow: [UIWindowScene: AlertWindow] = [:] static var alertWindow: [UIWindowScene: AlertWindow] = [:]
static var sheetWindows: [UIWindowScene: [SheetWindow]] = [:] static var sheetWindows: [UIWindowScene: [SheetWindow]] = [:]
static var capsuleWindows: [UIWindowScene: [CapsuleTarget.ViewModel.Position: CapsuleWindow]] = [:] static var capsuleWindows: [UIWindowScene: [CapsuleViewModel.Position: CapsuleWindow]] = [:]
static var current: AppContext? { static var current: AppContext? {
guard let windowScene = windowScene else { return nil } guard let windowScene = windowScene else { return nil }
@ -120,7 +120,7 @@ extension AppContext {
var toastWindows: [ToastWindow] { var toastWindows: [ToastWindow] {
Self.toastWindows[windowScene] ?? [] Self.toastWindows[windowScene] ?? []
} }
var capsuleWindows: [CapsuleTarget.ViewModel.Position: CapsuleWindow] { var capsuleWindows: [CapsuleViewModel.Position: CapsuleWindow] {
Self.capsuleWindows[windowScene] ?? [:] Self.capsuleWindows[windowScene] ?? [:]
} }
} }

View File

@ -17,7 +17,7 @@ extension SheetTarget: DefaultLayout {
if self.cfg.customReloadData?(self) == true { if self.cfg.customReloadData?(self) == true {
return return
} }
view.tintColor = vm.tintColor ?? config.tintColor view.tintColor = vm?.tintColor ?? config.tintColor
// background // background
if backgroundView.superview == nil { if backgroundView.superview == nil {
view.insertSubview(backgroundView, at: 0) view.insertSubview(backgroundView, at: 0)

View File

@ -8,12 +8,12 @@
import UIKit import UIKit
open class SheetProvider: HUDProvider<SheetViewModel, SheetTarget> { open class SheetProvider: HUDProvider<SheetViewModel, SheetTarget> {
@discardableResult public required init(_ vm: ViewModel?, initializer: ((_ sheet: Target) -> Void)?) {
super.init(vm, initializer: initializer)
}
@discardableResult public required convenience init(initializer: ((_ sheet: Target) -> Void)?) { public typealias ViewModel = SheetViewModel
self.init(nil, initializer: initializer) public typealias Target = SheetTarget
@discardableResult @objc public required init(initializer: ((_ sheet: Target) -> Void)?) {
super.init(initializer: initializer)
} }
/// HUD /// HUD

View File

@ -41,7 +41,7 @@ open class SheetTarget: BaseController, HUDTargetType {
} }
} }
public var vm: SheetViewModel = .init() public var vm: SheetViewModel? = nil
required public override init() { required public override init() {
super.init() super.init()

View File

@ -9,16 +9,6 @@ import UIKit
public class ToastConfiguration: CommonConfiguration { public class ToastConfiguration: CommonConfiguration {
///
public var margin = CGFloat(8)
var customInfoStack: ((_ stack: StackView) -> Void)?
public func customInfoStack(handler: @escaping (_ stack: StackView) -> Void) {
customInfoStack = handler
}
///
public var lineSpace = CGFloat(4)
static var customGlobalConfig: ((_ config: ToastConfiguration) -> Void)? static var customGlobalConfig: ((_ config: ToastConfiguration) -> Void)?
/// ///
@ -27,11 +17,21 @@ public class ToastConfiguration: CommonConfiguration {
customGlobalConfig = callback customGlobalConfig = callback
} }
/// ///
public var windowEdgeInset: CGFloat? public var defaultDuration: TimeInterval = 10
var windowEdgeInsetByDefault: CGFloat {
windowEdgeInset ?? 16 ///
public var marginX = CGFloat(8)
///
public var marginY = CGFloat(8)
var customInfoStack: ((_ stack: StackView) -> Void)?
public func customInfoStack(handler: @escaping (_ stack: StackView) -> Void) {
customInfoStack = handler
} }
///
public var lineSpace = CGFloat(4)
override var cardMaxWidthByDefault: CGFloat { override var cardMaxWidthByDefault: CGFloat {
cardMaxWidth ?? 500 cardMaxWidth ?? 500

View File

@ -17,7 +17,7 @@ extension ToastTarget: DefaultLayout {
if self.cfg.customReloadData?(self) == true { if self.cfg.customReloadData?(self) == true {
return return
} }
view.tintColor = vm.tintColor ?? config.tintColor view.tintColor = vm?.tintColor ?? config.tintColor
loadContentViewIfNeeded() loadContentViewIfNeeded()
loadContentMaskViewIfNeeded() loadContentMaskViewIfNeeded()
guard customView == nil else { guard customView == nil else {
@ -26,7 +26,7 @@ extension ToastTarget: DefaultLayout {
} }
return return
} }
if vm.icon != nil || vm.iconURL != nil { if vm?.icon != nil || vm?.iconURL != nil {
if imageView.superview == nil { if imageView.superview == nil {
infoStack.insertArrangedSubview(imageView, at: 0) infoStack.insertArrangedSubview(imageView, at: 0)
imageView.snp.makeConstraints { make in imageView.snp.makeConstraints { make in
@ -42,8 +42,8 @@ extension ToastTarget: DefaultLayout {
if textStack.superview == nil { if textStack.superview == nil {
infoStack.addArrangedSubview(textStack) infoStack.addArrangedSubview(textStack)
} }
let titleCount = vm.title?.count ?? 0 let titleCount = vm?.title?.count ?? 0
let bodyCount = vm.message?.count ?? 0 let bodyCount = vm?.message?.count ?? 0
if titleCount > 0 { if titleCount > 0 {
textStack.insertArrangedSubview(titleLabel, at: 0) textStack.insertArrangedSubview(titleLabel, at: 0)
if bodyCount > 0 { if bodyCount > 0 {
@ -79,14 +79,12 @@ extension ToastTarget: DefaultLayout {
bodyLabel.removeFromSuperview() bodyLabel.removeFromSuperview()
} }
// //
titleLabel.text = vm.title titleLabel.text = vm?.title
bodyLabel.text = vm.message bodyLabel.text = vm?.message
view.layoutIfNeeded() view.layoutIfNeeded()
// //
vm.timeoutHandler = DispatchWorkItem(block: { [weak self] in updateTimeoutDuration()
self?.pop()
})
setupImageView() setupImageView()
@ -132,6 +130,17 @@ extension ToastTarget {
} }
} }
private func updateTimeoutDuration() {
// 使
if vm?.duration == nil {
vm?.duration = config.defaultDuration
}
//
vm?.timeoutHandler = DispatchWorkItem(block: { [weak self] in
self?.pop()
})
}
func setupImageView() { func setupImageView() {
// //
stopRotate(animateLayer) stopRotate(animateLayer)
@ -141,11 +150,11 @@ extension ToastTarget {
// //
progressView?.removeFromSuperview() 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 { if let rotation = vm?.rotation {
startRotate(rotation) startRotate(rotation)
} }

View File

@ -50,7 +50,7 @@ extension ToastTarget {
// frame // frame
let cardEdgeInsets = config.cardEdgeInsetsByDefault let cardEdgeInsets = config.cardEdgeInsetsByDefault
let width = CGFloat.minimum(AppContext.appBounds.width - config.windowEdgeInsetByDefault - config.windowEdgeInsetByDefault, config.cardMaxWidthByDefault) let width = CGFloat.minimum(AppContext.appBounds.width - config.marginX - config.marginX, config.cardMaxWidthByDefault)
view.frame.size = CGSize(width: width, height: config.cardMaxHeightByDefault) view.frame.size = CGSize(width: width, height: config.cardMaxHeightByDefault)
titleLabel.sizeToFit() titleLabel.sizeToFit()
bodyLabel.sizeToFit() bodyLabel.sizeToFit()
@ -92,7 +92,7 @@ extension ToastTarget {
} else { } else {
consolePrint("代码漏洞已经没有toast了") consolePrint("代码漏洞已经没有toast了")
} }
vm.duration = nil vm?.duration = nil
setContextWindows(windows) setContextWindows(windows)
UIView.animateEaseOut(duration: config.animateDurationForBuildOutByDefault) { UIView.animateEaseOut(duration: config.animateDurationForBuildOutByDefault) {
window.transform = .init(translationX: 0, y: 0-20-window.maxY) window.transform = .init(translationX: 0, y: 0-20-window.maxY)
@ -124,21 +124,34 @@ fileprivate var updateToastsLayoutWorkItem: DispatchWorkItem?
fileprivate extension ToastWindow { fileprivate extension ToastWindow {
static func setToastWindowsLayout(windows: [ToastWindow]) { static func setToastWindowsLayout(windows: [ToastWindow]) {
var windows: [Window] = windows
if let win = AppContext.current?.capsuleWindows[.top] {
windows.insert(win, at: 0)
}
for (i, window) in windows.enumerated() { for (i, window) in windows.enumerated() {
let config = window.toast.config let margin: CGFloat
if let window = window as? ToastWindow {
margin = window.toast.config.marginY
} else if let window = window as? CapsuleWindow {
margin = window.safeAreaInsets.top
} else {
margin = 8
}
var y = window.frame.origin.y var y = window.frame.origin.y
if i == 0 { if i == 0 {
let topLayoutMargins = AppContext.appWindow?.layoutMargins.top ?? config.margin let topLayoutMargins = AppContext.appWindow?.safeAreaInsets.top ?? margin
y = max(topLayoutMargins - config.margin, config.margin) y = max(topLayoutMargins, margin)
} else { } else {
if i - 1 < windows.count && i > 0 { if i - 1 < windows.count && i > 0 {
y = config.margin + windows[i-1].frame.maxY y = margin + windows[i-1].frame.maxY
} else { } else {
y = config.margin y = margin
} }
} }
if let window = window as? ToastWindow {
window.maxY = y + window.frame.size.height window.maxY = y + window.frame.size.height
UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) { }
UIView.animateEaseOut(duration: 0.68) {
window.frame.origin.y = y window.frame.origin.y = y
} }
} }
@ -154,3 +167,10 @@ fileprivate extension ToastWindow {
} }
} }
extension ToastWindow {
static func updateToastWindowsLayout() {
let wins = AppContext.current?.toastWindows ?? []
updateToastWindowsLayout(windows: wins)
}
}

View File

@ -8,19 +8,37 @@
import UIKit import UIKit
open class ToastProvider: HUDProvider<ToastViewModel, ToastTarget> { open class ToastProvider: HUDProvider<ToastViewModel, ToastTarget> {
@discardableResult public required init(_ vm: ViewModel?, initializer: ((_ toast: Target) -> Void)?) {
super.init(vm, initializer: initializer) public typealias ViewModel = ToastViewModel
public typealias Target = ToastTarget
@discardableResult @objc public required init(initializer: ((_ toast: Target) -> Void)?) {
super.init(initializer: initializer)
} }
@discardableResult public required convenience init(initializer: ((_ toast: Target) -> Void)?) { @discardableResult public convenience init(_ vm: ViewModel, initializer: ((_ toast: Target) -> Void)?) {
self.init(nil, initializer: initializer) self.init { toast in
toast.vm = vm
initializer?(toast)
} }
}
/// ViewModelTarget
/// - Parameter vm:
@discardableResult public convenience init(_ vm: ViewModel) {
self.init(vm, initializer: nil)
}
@discardableResult @objc public convenience init(_ text: String) {
self.init(.message(text), initializer: nil)
}
/// HUD /// HUD
/// - Parameters: /// - Parameters:
/// - identifier: /// - identifier:
/// - handler: /// - handler:
public static func lazyPush(identifier: String? = nil, file: String = #file, line: Int = #line, handler: @escaping (_ toast: ToastTarget) -> Void, onExists: ((_ toast: ToastTarget) -> Void)? = nil) { @objc public static func lazyPush(identifier: String? = nil, file: String = #file, line: Int = #line, handler: @escaping (_ toast: ToastTarget) -> Void, onExists: ((_ toast: ToastTarget) -> Void)? = nil) {
let id = identifier ?? (file + "#\(line)") let id = identifier ?? (file + "#\(line)")
if let vc = find(identifier: id).last { if let vc = find(identifier: id).last {
vc.update(handler: onExists ?? handler) vc.update(handler: onExists ?? handler)
@ -35,7 +53,7 @@ open class ToastProvider: HUDProvider<ToastViewModel, ToastTarget> {
/// HUD /// HUD
/// - Parameter identifier: /// - Parameter identifier:
/// - Returns: HUD /// - Returns: HUD
@discardableResult public static func find(identifier: String, update handler: ((_ toast: ToastTarget) -> Void)? = nil) -> [ToastTarget] { @discardableResult @objc public static func find(identifier: String, update handler: ((_ toast: ToastTarget) -> Void)? = nil) -> [ToastTarget] {
let arr = AppContext.toastWindows.values.flatMap({ $0 }).compactMap({ $0.toast }).filter({ $0.identifier == identifier }) 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.update(handler: handler) })

View File

@ -84,14 +84,17 @@ open class ToastTarget: BaseController, HUDTargetType {
public var isRemovable = true public var isRemovable = true
/// ///
@objc public var vm = ToastViewModel() @objc public var vm: ToastViewModel?
private var tapActionCallback: ((_ toast: ToastTarget) -> Void)? private var tapActionCallback: ((_ toast: ToastTarget) -> Void)?
public override var title: String? { public override var title: String? {
didSet { didSet {
if let vm = vm {
vm.title = title vm.title = title
} else {
vm = .title(title)
}
} }
} }
@ -135,7 +138,7 @@ fileprivate extension ToastTarget {
/// ///
/// - Parameter sender: /// - Parameter sender:
@objc func _onPanGesture(_ sender: UIPanGestureRecognizer) { @objc func _onPanGesture(_ sender: UIPanGestureRecognizer) {
vm.timeoutTimer?.invalidate() vm?.timeoutTimer?.invalidate()
let point = sender.translation(in: sender.view) let point = sender.translation(in: sender.view)
window?.transform = .init(translationX: 0, y: point.y) window?.transform = .init(translationX: 0, y: point.y)
if sender.state == .recognized { if sender.state == .recognized {
@ -150,8 +153,8 @@ fileprivate extension ToastTarget {
UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) { UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) {
self.window?.transform = .identity self.window?.transform = .identity
} completion: { done in } completion: { done in
let d = self.vm.duration let d = self.vm?.duration
self.vm.duration = d self.vm?.duration = d
} }
} }
} }