mirror of https://github.com/xaoxuu/ProHUD
新增Capsule组件
This commit is contained in:
parent
4ffd6c5617
commit
3ede7ea236
|
@ -7,6 +7,7 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
CD2439342A82164E00A3BBF5 /* CapsuleVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2439332A82164E00A3BBF5 /* CapsuleVC.swift */; };
|
||||
CD6537BF28C3311B00A5981B /* ListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6537BE28C3311B00A5981B /* ListModel.swift */; };
|
||||
CD6537C128C35E1C00A5981B /* ListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6537C028C35E1C00A5981B /* ListVC.swift */; };
|
||||
CD6537C328C35E6200A5981B /* ToastVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6537C228C35E6200A5981B /* ToastVC.swift */; };
|
||||
|
@ -26,6 +27,7 @@
|
|||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
CD2439332A82164E00A3BBF5 /* CapsuleVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CapsuleVC.swift; sourceTree = "<group>"; };
|
||||
CD6537BE28C3311B00A5981B /* ListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListModel.swift; sourceTree = "<group>"; };
|
||||
CD6537C028C35E1C00A5981B /* ListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListVC.swift; sourceTree = "<group>"; };
|
||||
CD6537C228C35E6200A5981B /* ToastVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastVC.swift; sourceTree = "<group>"; };
|
||||
|
@ -86,6 +88,7 @@
|
|||
CD6537C228C35E6200A5981B /* ToastVC.swift */,
|
||||
CDB7A1CF28C32A7400E034D8 /* AlertVC.swift */,
|
||||
CD6537C428C35F2C00A5981B /* SheetVC.swift */,
|
||||
CD2439332A82164E00A3BBF5 /* CapsuleVC.swift */,
|
||||
CD8EEF4028BC5C7200E660EA /* Main.storyboard */,
|
||||
CD8EEF4328BC5C7300E660EA /* Assets.xcassets */,
|
||||
CD8EEF4528BC5C7300E660EA /* LaunchScreen.storyboard */,
|
||||
|
@ -184,6 +187,7 @@
|
|||
files = (
|
||||
CDA83DB928C601E60025F0DF /* TableHeaderView.swift in Sources */,
|
||||
CD6537C528C35F2C00A5981B /* SheetVC.swift in Sources */,
|
||||
CD2439342A82164E00A3BBF5 /* CapsuleVC.swift in Sources */,
|
||||
CD6537C328C35E6200A5981B /* ToastVC.swift in Sources */,
|
||||
CDB7A1D028C32A7400E034D8 /* AlertVC.swift in Sources */,
|
||||
CD6537C128C35E1C00A5981B /* ListVC.swift in Sources */,
|
||||
|
|
|
@ -15,7 +15,7 @@ class AlertVC: ListVC {
|
|||
// Uncomment the following line to preserve selection between presentations
|
||||
// self.clearsSelectionOnViewWillAppear = false
|
||||
|
||||
header.titleLabel.text = "ProHUD.Alert"
|
||||
title = "Alert"
|
||||
header.detailLabel.text = "弹窗控件,用于强阻塞性交互,用户必须做出选择或者等待结果才能进入下一步,当多个实例出现时,会以堆叠的形式显示,新的实例会在覆盖旧的实例上层。"
|
||||
|
||||
Alert.Configuration.shared { config in
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22146" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="06J-FN-U3n">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22152" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="06J-FN-U3n">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22122"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22127"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Alert-->
|
||||
<!--AlertVC-->
|
||||
<scene sceneID="LP0-RE-kvY">
|
||||
<objects>
|
||||
<tableViewController id="NBo-Re-tKO" customClass="AlertVC" customModule="PHDemo" customModuleProvider="target" sceneMemberID="viewController">
|
||||
|
@ -32,11 +32,11 @@
|
|||
<outlet property="delegate" destination="NBo-Re-tKO" id="1L3-SV-7FG"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<tabBarItem key="tabBarItem" title="Alert" image="exclamationmark.triangle.fill" catalog="system" id="pLJ-z8-SS1"/>
|
||||
<navigationItem key="navigationItem" id="vza-Sb-cyH"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="ydp-D5-Zdx" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1865" y="804"/>
|
||||
<point key="canvasLocation" x="3684.057971014493" y="803.57142857142856"/>
|
||||
</scene>
|
||||
<!--Tab Bar Controller-->
|
||||
<scene sceneID="ej2-I3-4Bd">
|
||||
|
@ -50,16 +50,17 @@
|
|||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</tabBar>
|
||||
<connections>
|
||||
<segue destination="h7R-Kd-Dn5" kind="relationship" relationship="viewControllers" id="4rA-b5-Kd6"/>
|
||||
<segue destination="NBo-Re-tKO" kind="relationship" relationship="viewControllers" id="4BA-vv-RD2"/>
|
||||
<segue destination="9SY-ag-pK6" kind="relationship" relationship="viewControllers" id="vVd-PW-h6L"/>
|
||||
<segue destination="N1b-1U-hgP" kind="relationship" relationship="viewControllers" id="4rA-b5-Kd6"/>
|
||||
<segue destination="XXi-nT-rRc" kind="relationship" relationship="viewControllers" id="4BA-vv-RD2"/>
|
||||
<segue destination="U9P-t8-UPd" kind="relationship" relationship="viewControllers" id="vVd-PW-h6L"/>
|
||||
<segue destination="gUp-HF-Hqk" kind="relationship" relationship="viewControllers" id="Ven-DH-gtQ"/>
|
||||
</connections>
|
||||
</tabBarController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="avc-BE-wZC" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1865.217391304348" y="71.651785714285708"/>
|
||||
</scene>
|
||||
<!--Toast-->
|
||||
<!--ToastVC-->
|
||||
<scene sceneID="DAh-i5-GcF">
|
||||
<objects>
|
||||
<tableViewController id="h7R-Kd-Dn5" customClass="ToastVC" customModule="PHDemo" customModuleProvider="target" sceneMemberID="viewController">
|
||||
|
@ -83,13 +84,13 @@
|
|||
<outlet property="delegate" destination="h7R-Kd-Dn5" id="8Px-ei-ipU"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<tabBarItem key="tabBarItem" title="Toast" image="bubble.left.fill" catalog="system" id="YYs-U3-EWo"/>
|
||||
<navigationItem key="navigationItem" id="CTm-r7-VWj"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="VmT-Tm-s4d" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="954" y="804"/>
|
||||
<point key="canvasLocation" x="1863.7681159420292" y="803.57142857142856"/>
|
||||
</scene>
|
||||
<!--Sheet-->
|
||||
<!--SheetVC-->
|
||||
<scene sceneID="DG9-RE-7gC">
|
||||
<objects>
|
||||
<tableViewController id="9SY-ag-pK6" customClass="SheetVC" customModule="PHDemo" customModuleProvider="target" sceneMemberID="viewController">
|
||||
|
@ -113,19 +114,126 @@
|
|||
<outlet property="delegate" destination="9SY-ag-pK6" id="UkD-l4-OhM"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<tabBarItem key="tabBarItem" title="Sheet" image="iphone" catalog="system" id="3or-OI-jbb"/>
|
||||
<navigationItem key="navigationItem" id="Mf8-vT-Rhv"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Pal-Bf-SfP" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2742" y="804"/>
|
||||
<point key="canvasLocation" x="5471.0144927536239" y="803.57142857142856"/>
|
||||
</scene>
|
||||
<!--CapsuleVC-->
|
||||
<scene sceneID="Bhf-XS-ITh">
|
||||
<objects>
|
||||
<tableViewController id="xyo-OI-w9X" customClass="CapsuleVC" customModule="PHDemo" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="insetGrouped" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" estimatedSectionHeaderHeight="-1" sectionFooterHeight="18" estimatedSectionFooterHeight="-1" id="jta-3v-OTY">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="reuseIdentifier" id="ghs-Pa-9eL">
|
||||
<rect key="frame" x="20" y="55.5" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ghs-Pa-9eL" id="28C-qc-X0l">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343.5" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<sections/>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="xyo-OI-w9X" id="nD4-u6-Vmn"/>
|
||||
<outlet property="delegate" destination="xyo-OI-w9X" id="xH8-rN-VjG"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" id="ye2-RQ-uQG"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="O9X-HU-Yb0" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="7282.6086956521749" y="803.57142857142856"/>
|
||||
</scene>
|
||||
<!--Capsule-->
|
||||
<scene sceneID="L6I-bD-l0i">
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="gUp-HF-Hqk" sceneMemberID="viewController">
|
||||
<tabBarItem key="tabBarItem" title="Capsule" image="capsule.inset.filled" catalog="system" id="TpM-bR-LUV"/>
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" id="UzI-ky-yFv">
|
||||
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
<connections>
|
||||
<segue destination="xyo-OI-w9X" kind="relationship" relationship="rootViewController" id="hQP-fq-eZ3"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="zJ7-JR-Ywx" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="6372.4637681159429" y="803.57142857142856"/>
|
||||
</scene>
|
||||
<!--Sheet-->
|
||||
<scene sceneID="hej-Jg-pJw">
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="U9P-t8-UPd" sceneMemberID="viewController">
|
||||
<tabBarItem key="tabBarItem" title="Sheet" image="square.bottomthird.inset.filled" catalog="system" id="3or-OI-jbb"/>
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" id="ocF-2w-0SZ">
|
||||
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
<connections>
|
||||
<segue destination="9SY-ag-pK6" kind="relationship" relationship="rootViewController" id="hYH-5O-2G2"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="11x-h4-csl" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="4560.8695652173919" y="803.57142857142856"/>
|
||||
</scene>
|
||||
<!--Alert-->
|
||||
<scene sceneID="LMI-vK-E1X">
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="XXi-nT-rRc" sceneMemberID="viewController">
|
||||
<tabBarItem key="tabBarItem" title="Alert" image="exclamationmark.circle.fill" catalog="system" id="pLJ-z8-SS1"/>
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" id="2p6-UA-EMg">
|
||||
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
<connections>
|
||||
<segue destination="NBo-Re-tKO" kind="relationship" relationship="rootViewController" id="rn4-yf-GQT"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Wn1-r8-7Ki" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2773.913043478261" y="803.57142857142856"/>
|
||||
</scene>
|
||||
<!--Toast-->
|
||||
<scene sceneID="r0g-zP-tPJ">
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="N1b-1U-hgP" sceneMemberID="viewController">
|
||||
<tabBarItem key="tabBarItem" title="Toast" image="square.topthird.inset.filled" catalog="system" id="YYs-U3-EWo"/>
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" id="7e6-Ka-UlW">
|
||||
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
<connections>
|
||||
<segue destination="h7R-Kd-Dn5" kind="relationship" relationship="rootViewController" id="B3Q-IY-9gI"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="cJI-hc-E7h" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="953.62318840579712" y="803.57142857142856"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="bubble.left.fill" catalog="system" width="128" height="110"/>
|
||||
<image name="exclamationmark.triangle.fill" catalog="system" width="128" height="109"/>
|
||||
<image name="iphone" catalog="system" width="112" height="128"/>
|
||||
<image name="capsule.inset.filled" catalog="system" width="128" height="96"/>
|
||||
<image name="exclamationmark.circle.fill" catalog="system" width="128" height="123"/>
|
||||
<image name="square.bottomthird.inset.filled" catalog="system" width="128" height="114"/>
|
||||
<image name="square.topthird.inset.filled" catalog="system" width="128" height="114"/>
|
||||
<systemColor name="systemGroupedBackgroundColor">
|
||||
<color red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
//
|
||||
// CapsuleVC.swift
|
||||
// PHDemo
|
||||
//
|
||||
// Created by xaoxuu on 2022/9/3.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import ProHUD
|
||||
|
||||
class CapsuleVC: ListVC {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
title = "Capsule"
|
||||
header.detailLabel.text = "状态胶囊控件,用于状态显示,一个主程序窗口每个位置(上中下)各自最多只有一个状态胶囊实例。"
|
||||
|
||||
Capsule.Configuration.shared { config in
|
||||
// config.cardCornerRadius = .infinity // 设置一个较大的数字就会变成胶囊形状
|
||||
}
|
||||
list.add(title: "默认布局:纯文字") { section in
|
||||
section.add(title: "一条简短的消息") {
|
||||
Capsule(.message("一条简短的消息")).push()
|
||||
}
|
||||
section.add(title: "一条稍微长一点的消息") {
|
||||
Capsule(.message("一条稍微长一点的消息")).push()
|
||||
}
|
||||
section.add(title: "(默认)状态胶囊控件,用于状态显示,一个主程序窗口只有一个状态胶囊实例。") {
|
||||
Capsule(.message("状态胶囊控件,用于状态显示,一个主程序窗口只有一个状态胶囊实例。")).push()
|
||||
}
|
||||
section.add(title: "(限制1行)状态胶囊控件,用于状态显示,一个主程序窗口只有一个状态胶囊实例。") {
|
||||
Capsule(.message("状态胶囊控件,用于状态显示,一个主程序窗口只有一个状态胶囊实例。")) { capsule in
|
||||
capsule.config.customTextLabel { label in
|
||||
label.numberOfLines = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list.add(title: "默认布局:图文") { section in
|
||||
section.add(title: "一条简短的消息") {
|
||||
Capsule(.info("一条简短的消息")).push()
|
||||
}
|
||||
section.add(title: "一条稍微长一点的消息") {
|
||||
Capsule(.info("一条稍微长一点的消息")).push()
|
||||
}
|
||||
section.add(title: "(默认)状态胶囊控件,用于状态显示,一个主程序窗口只有一个状态胶囊实例。") {
|
||||
Capsule(.info("状态胶囊控件,用于状态显示,一个主程序窗口只有一个状态胶囊实例。")).push()
|
||||
}
|
||||
section.add(title: "(限制1行)状态胶囊控件,用于状态显示,一个主程序窗口只有一个状态胶囊实例。") {
|
||||
Capsule(.info("状态胶囊控件,用于状态显示,一个主程序窗口只有一个状态胶囊实例。")) { capsule in
|
||||
capsule.config.customTextLabel { label in
|
||||
label.numberOfLines = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list.add(title: "不同位置、不同动画") { section in
|
||||
section.add(title: "顶部,缩放") {
|
||||
Capsule(.info("一条简短的消息")) { capsule in
|
||||
capsule.config.animateBuildIn { window, completion in
|
||||
let duration = 1.0
|
||||
let d0 = duration * 0.2
|
||||
let d1 = duration
|
||||
window.transform = .init(scaleX: 0.001, y: 0.001)
|
||||
window.alpha = 0
|
||||
UIView.animate(withDuration: d0, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0.5) {
|
||||
window.transform = .init(scaleX: 0.01, y: 0.5)
|
||||
}
|
||||
UIView.animate(withDuration: d1, delay: d0 * 0.2, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5) {
|
||||
window.transform = .identity
|
||||
} completion: { done in
|
||||
completion()
|
||||
}
|
||||
UIView.animate(withDuration: duration * 0.4, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1) {
|
||||
window.alpha = 1
|
||||
}
|
||||
}
|
||||
capsule.config.animateBuildOut { window, completion in
|
||||
let duration = 0.8
|
||||
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1) {
|
||||
window.transform = .init(scaleX: 0.0001, y: 0.5)
|
||||
} completion: { done in
|
||||
completion()
|
||||
}
|
||||
UIView.animate(withDuration: duration * 0.6, delay: duration * 0.4, usingSpringWithDamping: 1, initialSpringVelocity: 1) {
|
||||
window.alpha = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
section.add(title: "中间,黑底白字,透明渐变") {
|
||||
Capsule(.middle.info("一条简短的消息")) { capsule in
|
||||
capsule.config.tintColor = .white
|
||||
capsule.config.cardCornerRadius = 4
|
||||
capsule.config.contentViewMask { mask in
|
||||
mask.layer.backgroundColor = UIColor.black.withAlphaComponent(0.75).cgColor
|
||||
}
|
||||
capsule.config.customTextLabel { label in
|
||||
label.textColor = .white
|
||||
}
|
||||
capsule.config.animateBuildIn { window, completion in
|
||||
window.alpha = 0
|
||||
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0.5) {
|
||||
window.alpha = 1
|
||||
} completion: { done in
|
||||
completion()
|
||||
}
|
||||
}
|
||||
capsule.config.animateBuildOut { window, completion in
|
||||
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0.5) {
|
||||
window.alpha = 0
|
||||
} completion: { done in
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
section.add(title: "底部,渐变背景,回弹滑入") {
|
||||
Capsule(.bottom.enter("点击进入")) { capsule in
|
||||
capsule.config.tintColor = .white
|
||||
capsule.config.customTextLabel { label in
|
||||
label.textColor = .white
|
||||
}
|
||||
capsule.config.contentViewMask { mask in
|
||||
mask.effect = .none
|
||||
mask.backgroundColor = .clear
|
||||
let gradientLayer = CAGradientLayer()
|
||||
gradientLayer.frame = self.view.bounds
|
||||
gradientLayer.colors = [UIColor("#0091FF").cgColor, UIColor("#00FDFF").cgColor]
|
||||
gradientLayer.startPoint = .init(x: 0.2, y: 0.6)
|
||||
gradientLayer.endPoint = .init(x: 0.6, y: 0.2)
|
||||
gradientLayer.frame = .init(x: 0, y: 0, width: 300, height: 100)
|
||||
mask.layer.sublayers?.forEach({ $0.removeFromSuperlayer() })
|
||||
mask.layer.insertSublayer(gradientLayer, at: 0)
|
||||
}
|
||||
capsule.config.cardCornerRadius = .infinity
|
||||
capsule.config.animateBuildIn { window, completion in
|
||||
window.transform = .init(translationX: 0, y: 240)
|
||||
window.alpha = 0
|
||||
UIView.animate(withDuration: 0.8, delay: 0, usingSpringWithDamping: 0.75, initialSpringVelocity: 0.5) {
|
||||
window.transform = .identity
|
||||
window.alpha = 1
|
||||
} completion: { done in
|
||||
completion()
|
||||
}
|
||||
}
|
||||
capsule.config.animateBuildOut { window, completion in
|
||||
UIView.animate(withDuration: 0.8, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0.5) {
|
||||
window.transform = .init(translationX: 0, y: 240)
|
||||
window.alpha = 0
|
||||
} completion: { done in
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}.onTapped { capsule in
|
||||
Alert(.message("收到点击事件").duration(1)).push()
|
||||
capsule.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Capsule.CapsuleViewModel {
|
||||
|
||||
static func info(_ text: String?) -> Self {
|
||||
.init()
|
||||
.info(text)
|
||||
}
|
||||
|
||||
func info(_ text: String?) -> Self {
|
||||
self.message(text)
|
||||
.icon(.init(systemName: "info.circle.fill"))
|
||||
// .duration(2)
|
||||
}
|
||||
|
||||
func enter(_ text: String?) -> Self {
|
||||
self.message(text)
|
||||
.icon(.init(systemName: "arrow.right.circle.fill"))
|
||||
.duration(.infinity)
|
||||
}
|
||||
|
||||
}
|
|
@ -12,7 +12,7 @@ class ListVC: UITableViewController {
|
|||
|
||||
var list = ListModel()
|
||||
|
||||
lazy var header: TableHeaderView = TableHeaderView(text: "ProHUD")
|
||||
lazy var header: TableHeaderView = TableHeaderView()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
@ -21,8 +21,24 @@ class ListVC: UITableViewController {
|
|||
tableView.sectionHeaderHeight = 32
|
||||
tableView.sectionFooterHeight = 8
|
||||
|
||||
navigationController?.navigationBar.prefersLargeTitles = true
|
||||
navigationItem.leftBarButtonItem = .init(title: "ProHUD", style: .done, target: self, action: #selector(self.onTappedLeftBarButtonItem(_:)))
|
||||
navigationItem.rightBarButtonItem = .init(image: .init(systemName: "questionmark.circle.fill"), style: .done, target: self, action: #selector(self.onTappedRightBarButtonItem(_:)))
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@objc func onTappedLeftBarButtonItem(_ sender: UIBarButtonItem) {
|
||||
guard let url = URL(string: "https://github.com/xaoxuu/ProHUD") else { return }
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
|
||||
@objc func onTappedRightBarButtonItem(_ sender: UIBarButtonItem) {
|
||||
guard let url = URL(string: "https://xaoxuu.com/wiki/prohud/") else { return }
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
|
||||
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
// #warning Incomplete implementation, return the number of sections
|
||||
|
|
|
@ -13,7 +13,7 @@ class SheetVC: ListVC {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
header.titleLabel.text = "ProHUD.Sheet"
|
||||
title = "Sheet"
|
||||
header.detailLabel.text = "操作表控件,用于弱阻塞性交互。显示区域为从屏幕底部向上弹出的新图层,可以放置丰富的内容,自由度较高。"
|
||||
|
||||
list.add(title: "默认布局") { section in
|
||||
|
|
|
@ -9,16 +9,8 @@ import UIKit
|
|||
|
||||
class TableHeaderView: UIView {
|
||||
|
||||
lazy var titleLabel: UILabel = {
|
||||
let lb = UILabel(frame: .init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 80))
|
||||
lb.font = .systemFont(ofSize: 32, weight: .black)
|
||||
lb.textAlignment = .center
|
||||
lb.text = "ProHUD"
|
||||
return lb
|
||||
}()
|
||||
|
||||
lazy var detailLabel: UILabel = {
|
||||
let lb = UILabel(frame: .init(x: 0, y: 80, width: UIScreen.main.bounds.width, height: 120))
|
||||
let lb = UILabel(frame: .init(x: 0, y: 80, width: UIScreen.main.bounds.width, height: 80))
|
||||
lb.font = .systemFont(ofSize: 15, weight: .regular)
|
||||
lb.textAlignment = .justified
|
||||
lb.numberOfLines = 0
|
||||
|
@ -26,23 +18,18 @@ class TableHeaderView: UIView {
|
|||
return lb
|
||||
}()
|
||||
|
||||
convenience init(text: String) {
|
||||
self.init(frame: .init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 150))
|
||||
titleLabel.text = text
|
||||
convenience init() {
|
||||
self.init(frame: .init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 80))
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
addSubview(detailLabel)
|
||||
addSubview(titleLabel)
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.left.right.equalToSuperview().inset(24)
|
||||
make.top.equalToSuperview().offset(28)
|
||||
}
|
||||
|
||||
detailLabel.snp.makeConstraints { make in
|
||||
make.left.right.equalTo(titleLabel)
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(12)
|
||||
make.left.right.equalToSuperview().inset(24)
|
||||
make.top.equalToSuperview().offset(8)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -47,9 +47,11 @@ class ToastVC: ListVC {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
title = "Toast"
|
||||
|
||||
let title = "通知条控件"
|
||||
let message = "通知条控件,用于非阻塞性事件通知。显示效果如同原生通知,默认会自动消失,可以支持手势移除,有多条通知可以平铺并列显示。"
|
||||
header.titleLabel.text = "ProHUD.Toast"
|
||||
|
||||
header.detailLabel.text = message
|
||||
|
||||
Toast.Configuration.shared { config in
|
||||
|
|
|
@ -82,11 +82,9 @@ open class Alert: ProHUD.Controller {
|
|||
}
|
||||
}
|
||||
|
||||
@discardableResult public init(_ vm: ViewModel?, handler: ((_ alert: Alert) -> Void)? = nil) {
|
||||
@discardableResult public init(_ vm: ViewModel, handler: ((_ alert: Alert) -> Void)? = nil) {
|
||||
super.init()
|
||||
if let vm = vm {
|
||||
self.vm = vm
|
||||
}
|
||||
self.vm = vm
|
||||
handler?(self)
|
||||
DispatchQueue.main.async {
|
||||
if handler != nil {
|
||||
|
@ -97,7 +95,7 @@ open class Alert: ProHUD.Controller {
|
|||
}
|
||||
|
||||
@discardableResult public convenience init(handler: ((_ alert: Alert) -> Void)?) {
|
||||
self.init(nil, handler: handler)
|
||||
self.init(.init(), handler: handler)
|
||||
}
|
||||
|
||||
public override func viewDidLoad() {
|
||||
|
|
|
@ -68,7 +68,7 @@ extension Alert: DefaultLayout {
|
|||
if contentView.superview != view {
|
||||
view.insertSubview(contentView, at: 0)
|
||||
}
|
||||
let alerts = window?.alerts ?? []
|
||||
let alerts = attachedWindow?.alerts ?? []
|
||||
if config.enableShadow && alerts.count > 0 {
|
||||
contentView.clipsToBounds = false
|
||||
contentView.layer.shadowRadius = 4
|
||||
|
@ -195,7 +195,11 @@ extension Alert {
|
|||
if bodyCount > 0 {
|
||||
config.customTitleLabel?(titleLabel)
|
||||
} else {
|
||||
config.customTextLabel?(titleLabel)
|
||||
if let customTextLabel = config.customTextLabel {
|
||||
customTextLabel(titleLabel)
|
||||
} else {
|
||||
titleLabel.font = .boldSystemFont(ofSize: 18)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if textStack.arrangedSubviews.contains(titleLabel) {
|
||||
|
@ -211,7 +215,11 @@ extension Alert {
|
|||
if titleCount > 0 {
|
||||
config.customBodyLabel?(bodyLabel)
|
||||
} else {
|
||||
config.customTextLabel?(bodyLabel)
|
||||
if let customTextLabel = config.customTextLabel {
|
||||
customTextLabel(bodyLabel)
|
||||
} else {
|
||||
bodyLabel.font = .boldSystemFont(ofSize: 18)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if textStack.arrangedSubviews.contains(bodyLabel) {
|
||||
|
@ -246,7 +254,7 @@ extension Alert {
|
|||
|
||||
public override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
let alerts = window?.alerts ?? []
|
||||
let alerts = attachedWindow?.alerts ?? []
|
||||
if config.enableShadow && alerts.count > 1 {
|
||||
contentView.layer.shadowPath = UIBezierPath.init(rect: contentView.bounds).cgPath
|
||||
}
|
||||
|
|
|
@ -39,7 +39,6 @@ extension Alert: HUD {
|
|||
|
||||
@objc open func pop() {
|
||||
navEvents[.onViewWillDisappear]?(self)
|
||||
let window = window ?? createAttachedWindowIfNotExists()
|
||||
Alert.removeAlert(alert: self)
|
||||
let duration = config.animateDurationForBuildOut ?? config.animateDurationForBuildOutByDefault
|
||||
UIView.animateEaseOut(duration: duration) {
|
||||
|
@ -51,9 +50,10 @@ extension Alert: HUD {
|
|||
self.navEvents[.onViewDidDisappear]?(self)
|
||||
}
|
||||
// hide window
|
||||
guard let window = view.window as? AlertWindow, let windowScene = windowScene else { return }
|
||||
let count = window.alerts.count
|
||||
if count == 0 {
|
||||
self.window = nil
|
||||
AppContext.alertWindow[windowScene] = nil
|
||||
UIView.animateEaseOut(duration: duration) {
|
||||
window.backgroundView.alpha = 0
|
||||
} completion: { done in
|
||||
|
@ -106,6 +106,8 @@ public extension Alert {
|
|||
|
||||
}
|
||||
|
||||
// MARK: - layout
|
||||
|
||||
fileprivate extension Alert {
|
||||
static func updateAlertsLayout(alerts: [Alert]) {
|
||||
for (i, a) in alerts.reversed().enumerated() {
|
||||
|
@ -124,7 +126,7 @@ fileprivate extension Alert {
|
|||
}
|
||||
|
||||
static func removeAlert(alert: Alert) {
|
||||
guard var alerts = alert.window?.alerts else {
|
||||
guard var alerts = alert.attachedWindow?.alerts else {
|
||||
return
|
||||
}
|
||||
if alerts.count > 1 {
|
||||
|
@ -141,7 +143,7 @@ fileprivate extension Alert {
|
|||
} else {
|
||||
print("‼️代码漏洞:已经没有alert了")
|
||||
}
|
||||
alert.window?.alerts = alerts
|
||||
alert.attachedWindow?.alerts = alerts
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,23 +7,6 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
extension Alert {
|
||||
var window: AlertWindow? {
|
||||
get {
|
||||
guard let windowScene = windowScene else {
|
||||
return nil
|
||||
}
|
||||
return AppContext.alertWindow[windowScene]
|
||||
}
|
||||
set {
|
||||
guard let windowScene = windowScene else {
|
||||
return
|
||||
}
|
||||
AppContext.alertWindow[windowScene] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AlertWindow: Window {
|
||||
|
||||
var alerts: [Alert] = []
|
||||
|
@ -45,8 +28,14 @@ class AlertWindow: Window {
|
|||
AppContext.alertWindow[windowScene] = w
|
||||
}
|
||||
// 比原生alert层级低一点
|
||||
w.windowLevel = .init(rawValue: UIWindow.Level.alert.rawValue - 1)
|
||||
w.windowLevel = .phAlert
|
||||
return w
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Alert {
|
||||
var attachedWindow: AlertWindow? {
|
||||
view.window as? AlertWindow
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
//
|
||||
// Capsule.swift
|
||||
//
|
||||
//
|
||||
// Created by xaoxuu on 2022/9/8.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
open class Capsule: Controller {
|
||||
|
||||
public lazy var config: Configuration = {
|
||||
var cfg = Configuration()
|
||||
Configuration.customShared?(cfg)
|
||||
return cfg
|
||||
}()
|
||||
|
||||
/// 内容容器(imageView、textLabel)
|
||||
public lazy var contentStack: StackView = {
|
||||
let stack = StackView()
|
||||
stack.spacing = 8
|
||||
stack.distribution = .equalCentering
|
||||
stack.alignment = .fill
|
||||
stack.axis = .horizontal
|
||||
config.customContentStack?(stack)
|
||||
return stack
|
||||
}()
|
||||
|
||||
/// 图标
|
||||
public lazy var imageView: UIImageView = {
|
||||
let imgv = UIImageView()
|
||||
imgv.contentMode = .scaleAspectFit
|
||||
return imgv
|
||||
}()
|
||||
|
||||
/// 文本
|
||||
public lazy var textLabel: UILabel = {
|
||||
let lb = UILabel()
|
||||
lb.textColor = config.primaryLabelColor
|
||||
lb.font = .boldSystemFont(ofSize: 15)
|
||||
lb.textAlignment = .justified
|
||||
lb.numberOfLines = 2
|
||||
config.customTextLabel?(lb)
|
||||
return lb
|
||||
}()
|
||||
|
||||
open class CapsuleViewModel: ViewModel {
|
||||
|
||||
public enum Position {
|
||||
case top
|
||||
case middle
|
||||
case bottom
|
||||
}
|
||||
public var position: Position = .top
|
||||
|
||||
public static var top: Self {
|
||||
let obj = Self.init()
|
||||
obj.position = .top
|
||||
return obj
|
||||
}
|
||||
public static var middle: Self {
|
||||
let obj = Self.init()
|
||||
obj.position = .middle
|
||||
return obj
|
||||
}
|
||||
public static var bottom: Self {
|
||||
let obj = Self.init()
|
||||
obj.position = .bottom
|
||||
return obj
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// 视图模型
|
||||
public var vm = CapsuleViewModel()
|
||||
|
||||
private var tapActionCallback: ((_ capsule: Capsule) -> Void)?
|
||||
|
||||
@discardableResult public init(_ vm: CapsuleViewModel, handler: ((_ capsule: Capsule) -> Void)? = nil) {
|
||||
super.init()
|
||||
self.vm = vm
|
||||
handler?(self)
|
||||
DispatchQueue.main.async {
|
||||
if handler != nil {
|
||||
self.push()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult public convenience init(handler: ((_ capsule: Capsule) -> Void)?) {
|
||||
self.init(.init(), handler: handler)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.tintColor = config.tintColor
|
||||
view.layer.shadowRadius = 8
|
||||
view.layer.shadowOffset = .init(width: 0, height: 5)
|
||||
view.layer.shadowOpacity = 0.1
|
||||
|
||||
// 点击
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(_onTappedGesture(_:)))
|
||||
view.addGestureRecognizer(tap)
|
||||
|
||||
reloadData(animated: false)
|
||||
|
||||
navEvents[.onViewDidLoad]?(self)
|
||||
|
||||
}
|
||||
|
||||
public func onTapped(action: @escaping (_ capsule: Capsule) -> Void) {
|
||||
self.tapActionCallback = action
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fileprivate extension Capsule {
|
||||
|
||||
/// 点击事件
|
||||
/// - Parameter sender: 手势
|
||||
@objc func _onTappedGesture(_ sender: UITapGestureRecognizer) {
|
||||
tapActionCallback?(self)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// CapsuleConfiguration.swift
|
||||
//
|
||||
//
|
||||
// Created by xaoxuu on 2022/9/8.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public extension Capsule {
|
||||
|
||||
typealias CustomAnimateHandler = ((_ window: UIWindow, _ completion: @escaping () -> Void) -> Void)
|
||||
|
||||
class Configuration: ProHUD.Configuration {
|
||||
|
||||
static var customShared: ((_ config: Configuration) -> Void)?
|
||||
|
||||
/// 共享配置(只能设置一次,影响所有实例)
|
||||
/// - Parameter callback: 配置代码
|
||||
public static func shared(_ callback: @escaping (_ config: Configuration) -> Void) {
|
||||
customShared = callback
|
||||
}
|
||||
|
||||
override var cardCornerRadiusByDefault: CGFloat {
|
||||
cardCornerRadius ?? 16
|
||||
}
|
||||
|
||||
override var cardEdgeInsetsByDefault: UIEdgeInsets {
|
||||
cardEdgeInsets ?? .init(top: 12, left: 16, bottom: 12, right: 16)
|
||||
}
|
||||
|
||||
override var cardMaxWidthByDefault: CGFloat { cardMaxWidth ?? 320 }
|
||||
|
||||
override var cardMaxHeightByDefault: CGFloat { cardMaxHeight ?? 120 }
|
||||
|
||||
override var animateDurationForBuildInByDefault: CGFloat {
|
||||
animateDurationForBuildIn ?? 0.8
|
||||
}
|
||||
|
||||
override var animateDurationForBuildOutByDefault: CGFloat {
|
||||
animateDurationForBuildOut ?? 0.8
|
||||
}
|
||||
|
||||
var animateBuildIn: CustomAnimateHandler?
|
||||
public func animateBuildIn(_ handler: CustomAnimateHandler?) {
|
||||
animateBuildIn = handler
|
||||
}
|
||||
|
||||
var animateBuildOut: CustomAnimateHandler?
|
||||
public func animateBuildOut(_ handler: CustomAnimateHandler?) {
|
||||
animateBuildOut = handler
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
//
|
||||
// CapsuleDefaultLayout.swift
|
||||
//
|
||||
//
|
||||
// Created by xaoxuu on 2022/9/9.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension Capsule: DefaultLayout {
|
||||
|
||||
var cfg: ProHUD.Configuration {
|
||||
return config
|
||||
}
|
||||
|
||||
func reloadData(animated: Bool) {
|
||||
if self.cfg.customReloadData?(self) == true {
|
||||
return
|
||||
}
|
||||
|
||||
// content
|
||||
loadContentViewIfNeeded()
|
||||
loadContentMaskViewIfNeeded()
|
||||
|
||||
// image
|
||||
imageView.removeFromSuperview()
|
||||
if let icon = vm.icon {
|
||||
contentStack.insertArrangedSubview(imageView, at: 0)
|
||||
imageView.image = icon
|
||||
} else {
|
||||
contentStack.removeArrangedSubview(imageView)
|
||||
}
|
||||
|
||||
// text
|
||||
textLabel.removeFromSuperview()
|
||||
let text = (vm.title ?? "") + (vm.message ?? "")
|
||||
if text.count > 0 {
|
||||
contentStack.addArrangedSubview(textLabel)
|
||||
textLabel.snp.makeConstraints { make in
|
||||
make.width.lessThanOrEqualTo(AppContext.appBounds.width * 0.5)
|
||||
}
|
||||
textLabel.text = text
|
||||
textLabel.sizeToFit()
|
||||
} else {
|
||||
contentStack.removeArrangedSubview(textLabel)
|
||||
}
|
||||
|
||||
view.layoutIfNeeded()
|
||||
|
||||
// 更新时间
|
||||
updateTimeoutDuration()
|
||||
|
||||
if isViewDisplayed {
|
||||
UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func loadContentViewIfNeeded() {
|
||||
if contentView.superview != view {
|
||||
view.insertSubview(contentView, at: 0)
|
||||
}
|
||||
|
||||
// layout
|
||||
contentView.snp.remakeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
guard customView == nil else {
|
||||
if contentStack.superview != nil {
|
||||
contentStack.removeFromSuperview()
|
||||
}
|
||||
return
|
||||
}
|
||||
// stack
|
||||
if contentStack.superview == nil {
|
||||
view.addSubview(contentStack)
|
||||
contentStack.snp.remakeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func updateTimeoutDuration() {
|
||||
// 为空时使用默认规则
|
||||
if vm.duration == nil {
|
||||
vm.duration = 3
|
||||
}
|
||||
// 设置持续时间
|
||||
vm.timeoutHandler = DispatchWorkItem(block: { [weak self] in
|
||||
self?.pop()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
//
|
||||
// CapsuleManager.swift
|
||||
//
|
||||
//
|
||||
// Created by xaoxuu on 2022/9/8.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension Capsule: HUD {
|
||||
|
||||
@objc open func push() {
|
||||
guard Configuration.isEnabled else { return }
|
||||
let isNew: Bool
|
||||
let window: CapsuleWindow
|
||||
let position = vm.position
|
||||
|
||||
if let w = AppContext.current?.capsuleWindows[position] {
|
||||
isNew = false
|
||||
window = w
|
||||
} else {
|
||||
window = CapsuleWindow(capsule: self)
|
||||
isNew = true
|
||||
}
|
||||
// frame
|
||||
let cardEdgeInsetsByDefault = config.cardEdgeInsetsByDefault
|
||||
view.layoutIfNeeded()
|
||||
var size = contentStack.frame.size
|
||||
size = CGSize(width: min(config.cardMaxWidthByDefault, size.width + cardEdgeInsetsByDefault.left + cardEdgeInsetsByDefault.right), height: min(config.cardMaxHeightByDefault, size.height + cardEdgeInsetsByDefault.top + cardEdgeInsetsByDefault.bottom))
|
||||
|
||||
// 应用到frame
|
||||
let newFrame: CGRect
|
||||
switch vm.position {
|
||||
case .top:
|
||||
let topLayoutMargins = AppContext.appWindow?.layoutMargins.top ?? 8
|
||||
let y = max(topLayoutMargins - 8, 8)
|
||||
newFrame = .init(x: (AppContext.appBounds.width - size.width) / 2, y: y, width: size.width, height: size.height)
|
||||
case .middle:
|
||||
newFrame = .init(x: (AppContext.appBounds.width - size.width) / 2, y: (AppContext.appBounds.height - size.height) / 2 - 20, width: size.width, height: size.height)
|
||||
case .bottom:
|
||||
let bottomLayoutMargins = AppContext.appWindow?.layoutMargins.bottom ?? 8
|
||||
let y = AppContext.appBounds.height - bottomLayoutMargins - size.height - 60
|
||||
newFrame = .init(x: (AppContext.appBounds.width - size.width) / 2, y: y, width: size.width, height: size.height)
|
||||
}
|
||||
|
||||
window.transform = .identity
|
||||
if isNew {
|
||||
window.frame = newFrame
|
||||
}
|
||||
|
||||
config.cardCornerRadius = min(size.height / 2, config.cardCornerRadiusByDefault)
|
||||
contentView.layer.cornerRadiusWithContinuous = config.cardCornerRadiusByDefault
|
||||
view.layer.cornerRadiusWithContinuous = config.cardCornerRadiusByDefault
|
||||
|
||||
window.rootViewController = self // 此时toast.view.frame.size会自动更新为window.frame.size
|
||||
if let s = AppContext.windowScene {
|
||||
if AppContext.capsuleWindows[s] == nil {
|
||||
AppContext.capsuleWindows[s] = [:]
|
||||
}
|
||||
AppContext.capsuleWindows[s]?[position] = window
|
||||
}
|
||||
navEvents[.onViewWillAppear]?(self)
|
||||
if isNew {
|
||||
window.isHidden = false
|
||||
if let animateBuildIn = config.animateBuildIn {
|
||||
animateBuildIn(window) {
|
||||
self.navEvents[.onViewDidAppear]?(self)
|
||||
}
|
||||
} else {
|
||||
let duration = config.animateDurationForBuildInByDefault
|
||||
window.transform = .init(translationX: 0, y: -window.frame.maxY - 20)
|
||||
UIView.animateEaseOut(duration: duration) {
|
||||
window.transform = .identity
|
||||
} completion: { done in
|
||||
self.navEvents[.onViewDidAppear]?(self)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
view.layoutIfNeeded()
|
||||
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) {
|
||||
window.frame = newFrame
|
||||
window.layoutIfNeeded()
|
||||
} completion: { done in
|
||||
self.navEvents[.onViewDidAppear]?(self)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@objc open func pop() {
|
||||
guard let window = attachedWindow, let windowScene = windowScene else { return }
|
||||
AppContext.capsuleWindows[windowScene]?[vm.position] = nil
|
||||
navEvents[.onViewWillDisappear]?(self)
|
||||
if let animateBuildOut = config.animateBuildOut {
|
||||
animateBuildOut(window) {
|
||||
window.isHidden = true
|
||||
window.transform = .identity
|
||||
self.navEvents[.onViewDidAppear]?(self)
|
||||
}
|
||||
} else {
|
||||
let duration = config.animateDurationForBuildOutByDefault
|
||||
UIView.animateEaseOut(duration: duration) {
|
||||
window.transform = .init(translationX: 0, y: -window.frame.maxY - 20)
|
||||
} completion: { done in
|
||||
window.isHidden = true
|
||||
window.transform = .identity
|
||||
self.navEvents[.onViewDidDisappear]?(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public extension Capsule {
|
||||
|
||||
/// 如果不存在就创建并弹出一个HUD实例,如果存在就更新实例
|
||||
/// - Parameters:
|
||||
/// - identifier: 实例唯一标识符(如果为空,则以代码位置为唯一标识符)
|
||||
/// - handler: 实例创建代码
|
||||
static func lazyPush(identifier: String? = nil, file: String = #file, line: Int = #line, handler: @escaping (_ capsule: Capsule) -> Void, onExists: ((_ capsule: Capsule) -> Void)? = nil) {
|
||||
let id = identifier ?? (file + "#\(line)")
|
||||
if let vc = find(identifier: id).last {
|
||||
vc.update(handler: onExists ?? handler)
|
||||
} else {
|
||||
Capsule { capsule in
|
||||
capsule.identifier = id
|
||||
handler(capsule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新HUD实例
|
||||
/// - Parameter handler: 实例更新代码
|
||||
func update(handler: @escaping (_ capsule: Capsule) -> Void) {
|
||||
handler(self)
|
||||
reloadData()
|
||||
UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
/// 查找HUD实例
|
||||
/// - Parameter identifier: 唯一标识符
|
||||
/// - Returns: HUD实例
|
||||
@discardableResult static func find(identifier: String, update handler: ((_ capsule: Capsule) -> Void)? = nil) -> [Capsule] {
|
||||
let arr = AppContext.capsuleWindows.values.flatMap({ $0.values }).compactMap({ $0.capsule }).filter({ $0.identifier == identifier })
|
||||
if let handler = handler {
|
||||
arr.forEach({ $0.update(handler: handler) })
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
//extension Capsule {
|
||||
//
|
||||
// func translateIn(completion: (() -> Void)?) {
|
||||
// UIView.animateEaseOut(duration: config.animateDurationForBuildInByDefault) {
|
||||
// if self.config.stackDepthEffect {
|
||||
// if isPhonePortrait {
|
||||
// AppContext.appWindow?.transform = .init(translationX: 0, y: 8).scaledBy(x: 0.9, y: 0.9)
|
||||
// } else {
|
||||
// AppContext.appWindow?.transform = .init(scaleX: 0.92, y: 0.92)
|
||||
// }
|
||||
// AppContext.appWindow?.layer.cornerRadiusWithContinuous = 16
|
||||
// AppContext.appWindow?.layer.masksToBounds = true
|
||||
// }
|
||||
// } completion: { done in
|
||||
// completion?()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func translateOut(completion: (() -> Void)?) {
|
||||
// UIView.animateEaseOut(duration: config.animateDurationForBuildOutByDefault) {
|
||||
// if self.config.stackDepthEffect {
|
||||
// AppContext.appWindow?.transform = .identity
|
||||
// AppContext.appWindow?.layer.cornerRadius = 0
|
||||
// }
|
||||
// } completion: { done in
|
||||
// completion?()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//}
|
|
@ -0,0 +1,43 @@
|
|||
//
|
||||
// CapsuleWindow.swift
|
||||
//
|
||||
//
|
||||
// Created by xaoxuu on 2022/9/8.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class CapsuleWindow: Window {
|
||||
|
||||
var capsule: Capsule
|
||||
|
||||
init(capsule: Capsule) {
|
||||
self.capsule = capsule
|
||||
super.init(frame: .zero)
|
||||
windowScene = AppContext.windowScene
|
||||
switch capsule.vm.position {
|
||||
case .top:
|
||||
// 略高于toast
|
||||
windowLevel = .phCapsuleTop
|
||||
case .middle:
|
||||
// 略低于alert
|
||||
windowLevel = .phCapsuleMiddle
|
||||
case .bottom:
|
||||
// 略高于sheet
|
||||
windowLevel = .phCapsuleBottom
|
||||
}
|
||||
frame = .init(x: 0, y: 0, width: 128, height: 48)
|
||||
isHidden = false
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Capsule {
|
||||
var attachedWindow: CapsuleWindow? {
|
||||
view.window as? CapsuleWindow
|
||||
}
|
||||
}
|
|
@ -106,13 +106,11 @@ public class Configuration: NSObject {
|
|||
|
||||
// MARK: 文本样式
|
||||
|
||||
var customTextLabel: ((_ label: UILabel) -> Void)? = { label in
|
||||
label.font = .boldSystemFont(ofSize: 18)
|
||||
}
|
||||
var customTextLabel: ((_ label: UILabel) -> Void)?
|
||||
|
||||
/// 自定义文本标签(标题或正文)
|
||||
/// - Parameter handler: 自定义文本标签(标题或正文)
|
||||
public func customTextLabel(handler: @escaping (_ label: UILabel) -> Void) {
|
||||
public func customTextLabel(_ handler: @escaping (_ label: UILabel) -> Void) {
|
||||
customTextLabel = handler
|
||||
}
|
||||
|
||||
|
@ -120,7 +118,7 @@ public class Configuration: NSObject {
|
|||
|
||||
/// 自定义标题标签
|
||||
/// - Parameter handler: 自定义标题标签
|
||||
public func customTitleLabel(handler: @escaping (_ label: UILabel) -> Void) {
|
||||
public func customTitleLabel(_ handler: @escaping (_ label: UILabel) -> Void) {
|
||||
customTitleLabel = handler
|
||||
}
|
||||
|
||||
|
@ -129,7 +127,7 @@ public class Configuration: NSObject {
|
|||
|
||||
/// 自定义正文标签
|
||||
/// - Parameter handler: 自定义正文标签
|
||||
public func customBodyLabel(handler: @escaping (_ label: UILabel) -> Void) {
|
||||
public func customBodyLabel(_ handler: @escaping (_ label: UILabel) -> Void) {
|
||||
customBodyLabel = handler
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,10 @@ open class ViewModel: NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
public required override init() {
|
||||
|
||||
}
|
||||
|
||||
public convenience init(icon: UIImage? = nil, duration: TimeInterval? = nil) {
|
||||
self.init()
|
||||
self.icon = icon
|
||||
|
@ -62,83 +66,95 @@ open class ViewModel: NSObject {
|
|||
// MARK: - convenience func
|
||||
public extension ViewModel {
|
||||
|
||||
func icon(_ image: UIImage?) -> ViewModel {
|
||||
func icon(_ image: UIImage?) -> Self {
|
||||
self.icon = image
|
||||
return self
|
||||
}
|
||||
|
||||
func icon(_ imageURL: URL?) -> ViewModel {
|
||||
func icon(_ imageURL: URL?) -> Self {
|
||||
self.iconURL = imageURL
|
||||
return self
|
||||
}
|
||||
|
||||
|
||||
func title(_ text: String?) -> ViewModel {
|
||||
func title(_ text: String?) -> Self {
|
||||
self.title = text
|
||||
return self
|
||||
}
|
||||
|
||||
func message(_ text: String?) -> ViewModel {
|
||||
func message(_ text: String?) -> Self {
|
||||
self.message = text
|
||||
return self
|
||||
}
|
||||
|
||||
func duration(_ seconds: TimeInterval?) -> ViewModel {
|
||||
func duration(_ seconds: TimeInterval?) -> Self {
|
||||
self.duration = seconds
|
||||
return self
|
||||
}
|
||||
|
||||
func rotation(_ rotation: Rotation?) -> Self {
|
||||
self.rotation = rotation
|
||||
return self
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - example scenes
|
||||
public extension ViewModel {
|
||||
|
||||
// MARK: plain
|
||||
static func title(_ text: String?) -> ViewModel {
|
||||
let obj = ViewModel()
|
||||
obj.title = text
|
||||
return obj
|
||||
static func title(_ text: String?) -> Self {
|
||||
.init()
|
||||
.title(text)
|
||||
}
|
||||
static func message(_ text: String?) -> ViewModel {
|
||||
let obj = ViewModel()
|
||||
obj.message = text
|
||||
return obj
|
||||
static func message(_ text: String?) -> Self {
|
||||
.init()
|
||||
.message(text)
|
||||
}
|
||||
|
||||
// MARK: loading
|
||||
static var loading: ViewModel {
|
||||
let obj = ViewModel(icon: UIImage(inProHUD: "prohud.windmill"))
|
||||
obj.rotation = .init(repeatCount: .infinity)
|
||||
return obj
|
||||
static var loading: Self {
|
||||
.init()
|
||||
.icon(.init(inProHUD: "prohud.windmill"))
|
||||
.rotation(.init(repeatCount: .infinity))
|
||||
}
|
||||
static func loading(_ seconds: TimeInterval) -> ViewModel {
|
||||
let obj = ViewModel(icon: UIImage(inProHUD: "prohud.windmill"), duration: seconds)
|
||||
obj.rotation = .init(repeatCount: .infinity)
|
||||
return obj
|
||||
static func loading(_ seconds: TimeInterval) -> Self {
|
||||
.init()
|
||||
.icon(.init(inProHUD: "prohud.windmill"))
|
||||
.rotation(.init(repeatCount: .infinity))
|
||||
.duration(seconds)
|
||||
}
|
||||
// MARK: success
|
||||
static var success: ViewModel {
|
||||
.init(icon: UIImage(inProHUD: "prohud.checkmark"))
|
||||
static var success: Self {
|
||||
.init()
|
||||
.icon(.init(inProHUD: "prohud.checkmark"))
|
||||
}
|
||||
static func success(_ seconds: TimeInterval) -> ViewModel {
|
||||
.init(icon: UIImage(inProHUD: "prohud.checkmark"), duration: seconds)
|
||||
static func success(_ seconds: TimeInterval) -> Self {
|
||||
.init()
|
||||
.icon(.init(inProHUD: "prohud.checkmark"))
|
||||
.duration(seconds)
|
||||
}
|
||||
// MARK: warning
|
||||
static var warning: ViewModel {
|
||||
.init(icon: UIImage(inProHUD: "prohud.exclamationmark"))
|
||||
static var warning: Self {
|
||||
.init()
|
||||
.icon(.init(inProHUD: "prohud.exclamationmark"))
|
||||
}
|
||||
static func warning(_ seconds: TimeInterval) -> ViewModel {
|
||||
.init(icon: UIImage(inProHUD: "prohud.exclamationmark"), duration: seconds)
|
||||
static func warning(_ seconds: TimeInterval) -> Self {
|
||||
.init()
|
||||
.icon(.init(inProHUD: "prohud.exclamationmark"))
|
||||
.duration(seconds)
|
||||
}
|
||||
// MARK: error
|
||||
static var error: ViewModel {
|
||||
.init(icon: UIImage(inProHUD: "prohud.xmark"))
|
||||
static var error: Self {
|
||||
.init()
|
||||
.icon(.init(inProHUD: "prohud.xmark"))
|
||||
}
|
||||
static func error(_ seconds: TimeInterval) -> ViewModel {
|
||||
.init(icon: UIImage(inProHUD: "prohud.xmark"), duration: seconds)
|
||||
static func error(_ seconds: TimeInterval) -> Self {
|
||||
.init()
|
||||
.icon(.init(inProHUD: "prohud.xmark"))
|
||||
.duration(seconds)
|
||||
}
|
||||
// MARK: failure
|
||||
static var failure: ViewModel { error }
|
||||
static func failure(_ seconds: TimeInterval) -> ViewModel { error(seconds) }
|
||||
static var failure: Self { error }
|
||||
static func failure(_ seconds: TimeInterval) -> Self { error(seconds) }
|
||||
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ public struct AppContext {
|
|||
static var toastWindows: [UIWindowScene: [ToastWindow]] = [:]
|
||||
static var alertWindow: [UIWindowScene: AlertWindow] = [:]
|
||||
static var sheetWindows: [UIWindowScene: [SheetWindow]] = [:]
|
||||
static var capsuleWindows: [UIWindowScene: [Capsule.CapsuleViewModel.Position: CapsuleWindow]] = [:]
|
||||
|
||||
static var current: AppContext? {
|
||||
guard let windowScene = windowScene else { return nil }
|
||||
|
@ -116,10 +117,11 @@ extension AppContext {
|
|||
var sheetWindows: [SheetWindow] {
|
||||
Self.sheetWindows[windowScene] ?? []
|
||||
}
|
||||
}
|
||||
|
||||
extension AppContext {
|
||||
var toastWindows: [ToastWindow] {
|
||||
Self.toastWindows[windowScene] ?? []
|
||||
}
|
||||
var capsuleWindows: [Capsule.CapsuleViewModel.Position: CapsuleWindow] {
|
||||
Self.capsuleWindows[windowScene] ?? [:]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,3 +30,21 @@ var isPortrait: Bool {
|
|||
var isPhonePortrait: Bool {
|
||||
UIDevice.current.userInterfaceIdiom == .phone && (AppContext.windowScene?.interfaceOrientation.isPortrait == true)
|
||||
}
|
||||
|
||||
|
||||
// 层级: Capsule(top) -> Toast -> 原生Alert -> Alert -> Capsule(middle) -> Sheet -> Capsule(bottom)
|
||||
extension UIWindow.Level {
|
||||
|
||||
public static let phCapsuleTop: UIWindow.Level = .init(rawValue: UIWindow.Level.alert.rawValue + 1005)
|
||||
|
||||
public static let phToast: UIWindow.Level = .init(rawValue: UIWindow.Level.alert.rawValue + 1000)
|
||||
|
||||
public static let phAlert: UIWindow.Level = .init(rawValue: UIWindow.Level.alert.rawValue - 10)
|
||||
|
||||
public static let phCapsuleMiddle: UIWindow.Level = .init(rawValue: UIWindow.Level.alert.rawValue - 15)
|
||||
|
||||
public static let phSheet: UIWindow.Level = .init(rawValue: UIWindow.Level.alert.rawValue - 20)
|
||||
|
||||
public static let phCapsuleBottom: UIWindow.Level = .init(rawValue: UIWindow.Level.alert.rawValue - 25)
|
||||
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import UIKit
|
|||
extension UIView {
|
||||
|
||||
static func animateEaseOut(duration: TimeInterval, animations: @escaping () -> Void, completion: ((_ done: Bool) -> Void)? = nil) {
|
||||
animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: [.allowUserInteraction, .curveEaseOut], animations: animations, completion: completion)
|
||||
animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0.75, options: [.allowUserInteraction, .curveEaseOut], animations: animations, completion: completion)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ open class Sheet: Controller {
|
|||
handler(self)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
SheetWindow.push(sheet: self)
|
||||
self.push()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ extension Sheet {
|
|||
if let act = onTappedBackground {
|
||||
act(self)
|
||||
} else {
|
||||
SheetWindow.pop(sheet: self)
|
||||
self.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,11 +11,50 @@ extension Sheet: HUD {
|
|||
|
||||
@objc open func push() {
|
||||
guard Configuration.isEnabled else { return }
|
||||
SheetWindow.push(sheet: self)
|
||||
let isNew: Bool
|
||||
let window: SheetWindow
|
||||
var windows = AppContext.current?.sheetWindows ?? []
|
||||
if let w = windows.first(where: { $0.sheet == self }) {
|
||||
isNew = false
|
||||
window = w
|
||||
} else {
|
||||
window = SheetWindow(sheet: self)
|
||||
isNew = true
|
||||
}
|
||||
window.rootViewController = self
|
||||
if windows.contains(window) == false {
|
||||
windows.append(window)
|
||||
setContextWindows(windows)
|
||||
}
|
||||
if isNew {
|
||||
navEvents[.onViewWillAppear]?(self)
|
||||
window.sheet.translateIn { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.navEvents[.onViewDidAppear]?(self)
|
||||
}
|
||||
} else {
|
||||
view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
@objc open func pop() {
|
||||
SheetWindow.pop(sheet: self)
|
||||
var windows = getContextWindows()
|
||||
guard let window = windows.first(where: { $0.sheet == self }) else {
|
||||
return
|
||||
}
|
||||
navEvents[.onViewWillDisappear]?(self)
|
||||
window.sheet.translateOut { [weak window, weak self] in
|
||||
guard let self = self, let win = window else { return }
|
||||
win.sheet.navEvents[.onViewDidDisappear]?(win.sheet)
|
||||
if windows.count > 1 {
|
||||
windows.removeAll { $0 == win }
|
||||
} else if windows.count == 1 {
|
||||
windows.removeAll()
|
||||
} else {
|
||||
consolePrint("‼️代码漏洞:已经没有sheet了")
|
||||
}
|
||||
self.setContextWindows(windows)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,22 +7,6 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
|
||||
private extension Sheet {
|
||||
func getContextWindows() -> [SheetWindow] {
|
||||
guard let windowScene = windowScene else {
|
||||
return []
|
||||
}
|
||||
return AppContext.sheetWindows[windowScene] ?? []
|
||||
}
|
||||
func setContextWindows(_ windows: [SheetWindow]) {
|
||||
guard let windowScene = windowScene else {
|
||||
return
|
||||
}
|
||||
AppContext.sheetWindows[windowScene] = windows
|
||||
}
|
||||
}
|
||||
|
||||
class SheetWindow: Window {
|
||||
|
||||
var sheet: Sheet
|
||||
|
@ -35,7 +19,7 @@ class SheetWindow: Window {
|
|||
super.init(frame: AppContext.appBounds)
|
||||
}
|
||||
sheet.window = self
|
||||
windowLevel = .init(rawValue: UIWindow.Level.alert.rawValue - 2)
|
||||
windowLevel = .phSheet
|
||||
isHidden = false
|
||||
}
|
||||
|
||||
|
@ -43,52 +27,19 @@ class SheetWindow: Window {
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
static func push(sheet: Sheet) {
|
||||
let isNew: Bool
|
||||
let window: SheetWindow
|
||||
var windows = AppContext.current?.sheetWindows ?? []
|
||||
if let w = windows.first(where: { $0.sheet == sheet }) {
|
||||
isNew = false
|
||||
window = w
|
||||
} else {
|
||||
window = SheetWindow(sheet: sheet)
|
||||
isNew = true
|
||||
}
|
||||
window.rootViewController = sheet
|
||||
if windows.contains(window) == false {
|
||||
windows.append(window)
|
||||
sheet.setContextWindows(windows)
|
||||
}
|
||||
if isNew {
|
||||
sheet.navEvents[.onViewWillAppear]?(sheet)
|
||||
window.sheet.translateIn {
|
||||
sheet.navEvents[.onViewDidAppear]?(sheet)
|
||||
}
|
||||
} else {
|
||||
sheet.view.layoutIfNeeded()
|
||||
}
|
||||
|
||||
extension Sheet {
|
||||
func getContextWindows() -> [SheetWindow] {
|
||||
guard let windowScene = windowScene else {
|
||||
return []
|
||||
}
|
||||
return AppContext.sheetWindows[windowScene] ?? []
|
||||
}
|
||||
|
||||
static func pop(sheet: Sheet) {
|
||||
var windows = sheet.getContextWindows()
|
||||
guard let window = windows.first(where: { $0.sheet == sheet }) else {
|
||||
func setContextWindows(_ windows: [SheetWindow]) {
|
||||
guard let windowScene = windowScene else {
|
||||
return
|
||||
}
|
||||
sheet.navEvents[.onViewWillDisappear]?(sheet)
|
||||
window.sheet.translateOut { [weak window] in
|
||||
if let win = window {
|
||||
win.sheet.navEvents[.onViewDidDisappear]?(win.sheet)
|
||||
if windows.count > 1 {
|
||||
windows.removeAll { $0 == win }
|
||||
} else if windows.count == 1 {
|
||||
windows.removeAll()
|
||||
} else {
|
||||
consolePrint("‼️代码漏洞:已经没有sheet了")
|
||||
}
|
||||
sheet.setContextWindows(windows)
|
||||
}
|
||||
}
|
||||
AppContext.sheetWindows[windowScene] = windows
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ open class Toast: Controller {
|
|||
|
||||
public var progressView: ProgressView?
|
||||
|
||||
/// 内容容器(包括icon、textStack、actionStack)
|
||||
/// 内容容器(包括infoStack、actionStack)
|
||||
public lazy var contentStack: StackView = {
|
||||
let stack = StackView(axis: .vertical)
|
||||
stack.spacing = 16
|
||||
|
@ -27,7 +27,7 @@ open class Toast: Controller {
|
|||
return stack
|
||||
}()
|
||||
|
||||
/// 信息容器(image+text)
|
||||
/// 信息容器(imageView+textStack)
|
||||
public lazy var infoStack: StackView = {
|
||||
let stack = StackView(axis: .horizontal)
|
||||
stack.spacing = 8
|
||||
|
@ -36,7 +36,7 @@ open class Toast: Controller {
|
|||
return stack
|
||||
}()
|
||||
|
||||
/// 文本容器
|
||||
/// 文本容器(title、body)
|
||||
public lazy var textStack: StackView = {
|
||||
let stack = StackView(axis: .vertical)
|
||||
stack.spacing = config.lineSpace
|
||||
|
@ -96,21 +96,19 @@ open class Toast: Controller {
|
|||
}
|
||||
|
||||
|
||||
@discardableResult public init(_ vm: ViewModel?, handler: ((_ toast: Toast) -> Void)? = nil) {
|
||||
@discardableResult public init(_ vm: ViewModel, handler: ((_ toast: Toast) -> Void)? = nil) {
|
||||
super.init()
|
||||
if let vm = vm {
|
||||
self.vm = vm
|
||||
}
|
||||
self.vm = vm
|
||||
handler?(self)
|
||||
DispatchQueue.main.async {
|
||||
if handler != nil {
|
||||
ToastWindow.push(toast: self)
|
||||
self.push()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult public convenience init(handler: ((_ toast: Toast) -> Void)?) {
|
||||
self.init(nil, handler: handler)
|
||||
self.init(.init(), handler: handler)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
|
|
|
@ -48,7 +48,11 @@ extension Toast: DefaultLayout {
|
|||
if bodyCount > 0 {
|
||||
config.customTitleLabel?(titleLabel)
|
||||
} else {
|
||||
config.customTextLabel?(bodyLabel)
|
||||
if let customTextLabel = config.customTextLabel {
|
||||
customTextLabel(titleLabel)
|
||||
} else {
|
||||
titleLabel.font = .boldSystemFont(ofSize: 18)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if textStack.arrangedSubviews.contains(titleLabel) {
|
||||
|
@ -61,7 +65,11 @@ extension Toast: DefaultLayout {
|
|||
if titleCount > 0 {
|
||||
config.customBodyLabel?(bodyLabel)
|
||||
} else {
|
||||
config.customTextLabel?(bodyLabel)
|
||||
if let customTextLabel = config.customTextLabel {
|
||||
customTextLabel(bodyLabel)
|
||||
} else {
|
||||
bodyLabel.font = .boldSystemFont(ofSize: 18)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if textStack.arrangedSubviews.contains(bodyLabel) {
|
||||
|
|
|
@ -9,13 +9,99 @@ import UIKit
|
|||
|
||||
extension Toast: HUD {
|
||||
|
||||
private func calcHeight() -> CGFloat {
|
||||
var height = CGFloat(0)
|
||||
for v in infoStack.arrangedSubviews {
|
||||
// 图片或者文本最大高度
|
||||
height = CGFloat.maximum(v.frame.maxY, height)
|
||||
}
|
||||
if actionStack.arrangedSubviews.count > 0 {
|
||||
height += actionStack.frame.height + contentStack.spacing
|
||||
}
|
||||
contentView.subviews.filter { v in
|
||||
if v == contentMaskView {
|
||||
return false
|
||||
}
|
||||
if v == contentStack {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} .forEach { v in
|
||||
height = CGFloat.maximum(v.frame.maxY, height)
|
||||
}
|
||||
// 上下边间距
|
||||
let cardEdgeInsets = config.cardEdgeInsetsByDefault
|
||||
height += cardEdgeInsets.top + cardEdgeInsets.bottom
|
||||
return height
|
||||
}
|
||||
@objc open func push() {
|
||||
guard Configuration.isEnabled else { return }
|
||||
ToastWindow.push(toast: self)
|
||||
let isNew: Bool
|
||||
let window: ToastWindow
|
||||
var windows = AppContext.current?.toastWindows ?? []
|
||||
if let w = windows.first(where: { $0.toast == self }) {
|
||||
isNew = false
|
||||
window = w
|
||||
} else {
|
||||
window = ToastWindow(toast: self)
|
||||
isNew = true
|
||||
}
|
||||
|
||||
// frame
|
||||
let cardEdgeInsets = config.cardEdgeInsetsByDefault
|
||||
let width = CGFloat.minimum(AppContext.appBounds.width - config.windowEdgeInsetByDefault - config.windowEdgeInsetByDefault, config.cardMaxWidthByDefault)
|
||||
view.frame.size = CGSize(width: width, height: config.cardMaxHeightByDefault)
|
||||
titleLabel.sizeToFit()
|
||||
bodyLabel.sizeToFit()
|
||||
view.layoutIfNeeded()
|
||||
// 更新子视图之后获取正确的高度
|
||||
let height = calcHeight()
|
||||
view.frame.size = CGSize(width: width, height: height)
|
||||
// 应用到frame
|
||||
window.frame = CGRect(x: (AppContext.appBounds.width - width) / 2, y: 0, width: width, height: height)
|
||||
window.rootViewController = self // 此时toast.view.frame.size会自动更新为window.frame.size
|
||||
if windows.contains(window) == false {
|
||||
windows.append(window)
|
||||
setContextWindows(windows)
|
||||
}
|
||||
ToastWindow.updateToastWindowsLayout(windows: windows)
|
||||
if isNew {
|
||||
window.transform = .init(translationX: 0, y: -window.frame.maxY)
|
||||
UIView.animateEaseOut(duration: config.animateDurationForBuildInByDefault) {
|
||||
window.transform = .identity
|
||||
} completion: { done in
|
||||
self.navEvents[.onViewDidAppear]?(self)
|
||||
}
|
||||
} else {
|
||||
view.layoutIfNeeded()
|
||||
self.navEvents[.onViewDidAppear]?(self)
|
||||
}
|
||||
}
|
||||
|
||||
@objc open func pop() {
|
||||
ToastWindow.pop(toast: self)
|
||||
var windows = getContextWindows()
|
||||
guard let window = windows.first(where: { $0.toast == self }) else {
|
||||
return
|
||||
}
|
||||
if windows.count > 1 {
|
||||
windows.removeAll { $0 == window }
|
||||
ToastWindow.updateToastWindowsLayout(windows: windows)
|
||||
} else if windows.count == 1 {
|
||||
windows.removeAll()
|
||||
} else {
|
||||
consolePrint("‼️代码漏洞:已经没有toast了")
|
||||
}
|
||||
vm.duration = nil
|
||||
setContextWindows(windows)
|
||||
UIView.animateEaseOut(duration: config.animateDurationForBuildOutByDefault) {
|
||||
window.transform = .init(translationX: 0, y: 0-20-window.maxY)
|
||||
} completion: { done in
|
||||
self.view.removeFromSuperview()
|
||||
self.removeFromParent()
|
||||
self.navEvents[.onViewDidDisappear]?(self)
|
||||
// 这里设置一下window属性,会使window的生命周期被延长到此处,即动画执行过程中window不会被提前释放
|
||||
window.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -60,3 +146,41 @@ public extension Toast {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - layout
|
||||
|
||||
fileprivate var updateToastsLayoutWorkItem: DispatchWorkItem?
|
||||
|
||||
fileprivate extension ToastWindow {
|
||||
|
||||
static func setToastWindowsLayout(windows: [ToastWindow]) {
|
||||
for (i, window) in windows.enumerated() {
|
||||
let config = window.toast.config
|
||||
var y = window.frame.origin.y
|
||||
if i == 0 {
|
||||
let topLayoutMargins = AppContext.appWindow?.layoutMargins.top ?? config.margin
|
||||
y = max(topLayoutMargins - config.margin, config.margin)
|
||||
} else {
|
||||
if i - 1 < windows.count && i > 0 {
|
||||
y = config.margin + windows[i-1].frame.maxY
|
||||
} else {
|
||||
y = config.margin
|
||||
}
|
||||
}
|
||||
window.maxY = y + window.frame.size.height
|
||||
UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) {
|
||||
window.frame.origin.y = y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func updateToastWindowsLayout(windows: [ToastWindow]) {
|
||||
updateToastsLayoutWorkItem?.cancel()
|
||||
updateToastsLayoutWorkItem = DispatchWorkItem {
|
||||
setToastWindowsLayout(windows: windows)
|
||||
updateToastsLayoutWorkItem = nil
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now()+0.001, execute: updateToastsLayoutWorkItem!)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,21 +7,6 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
private extension Toast {
|
||||
func getContextWindows() -> [ToastWindow] {
|
||||
guard let windowScene = windowScene else {
|
||||
return []
|
||||
}
|
||||
return AppContext.toastWindows[windowScene] ?? []
|
||||
}
|
||||
func setContextWindows(_ windows: [ToastWindow]) {
|
||||
guard let windowScene = windowScene else {
|
||||
return
|
||||
}
|
||||
AppContext.toastWindows[windowScene] = windows
|
||||
}
|
||||
}
|
||||
|
||||
class ToastWindow: Window {
|
||||
|
||||
var toast: Toast
|
||||
|
@ -33,7 +18,7 @@ class ToastWindow: Window {
|
|||
super.init(frame: .zero)
|
||||
windowScene = AppContext.windowScene
|
||||
toast.window = self
|
||||
windowLevel = .init(rawValue: UIWindow.Level.alert.rawValue + 1000)
|
||||
windowLevel = .phToast
|
||||
layer.shadowRadius = 8
|
||||
layer.shadowOffset = .init(width: 0, height: 5)
|
||||
layer.shadowOpacity = 0.2
|
||||
|
@ -49,139 +34,19 @@ class ToastWindow: Window {
|
|||
layer.shadowPath = UIBezierPath.init(rect: bounds).cgPath
|
||||
}
|
||||
|
||||
static func push(toast: Toast) {
|
||||
let isNew: Bool
|
||||
let window: ToastWindow
|
||||
var windows = AppContext.current?.toastWindows ?? []
|
||||
if let w = windows.first(where: { $0.toast == toast }) {
|
||||
isNew = false
|
||||
window = w
|
||||
} else {
|
||||
window = ToastWindow(toast: toast)
|
||||
isNew = true
|
||||
}
|
||||
let config = toast.config
|
||||
|
||||
// frame
|
||||
let cardEdgeInsets = config.cardEdgeInsetsByDefault
|
||||
let width = CGFloat.minimum(AppContext.appBounds.width - config.windowEdgeInsetByDefault - config.windowEdgeInsetByDefault, config.cardMaxWidthByDefault)
|
||||
toast.view.frame.size = CGSize(width: width, height: config.cardMaxHeightByDefault)
|
||||
toast.titleLabel.sizeToFit()
|
||||
toast.bodyLabel.sizeToFit()
|
||||
toast.view.layoutIfNeeded()
|
||||
// 更新子视图之后获取正确的高度
|
||||
let height = toast.calcHeight()
|
||||
toast.view.frame.size = CGSize(width: width, height: height)
|
||||
// 应用到frame
|
||||
window.frame = CGRect(x: (AppContext.appBounds.width - width) / 2, y: 0, width: width, height: height)
|
||||
window.rootViewController = toast // 此时toast.view.frame.size会自动更新为window.frame.size
|
||||
if windows.contains(window) == false {
|
||||
windows.append(window)
|
||||
toast.setContextWindows(windows)
|
||||
}
|
||||
updateToastWindowsLayout(windows: windows)
|
||||
if isNew {
|
||||
window.transform = .init(translationX: 0, y: -window.frame.maxY)
|
||||
UIView.animateEaseOut(duration: config.animateDurationForBuildInByDefault) {
|
||||
window.transform = .identity
|
||||
} completion: { done in
|
||||
toast.navEvents[.onViewDidAppear]?(toast)
|
||||
}
|
||||
} else {
|
||||
toast.view.layoutIfNeeded()
|
||||
toast.navEvents[.onViewDidAppear]?(toast)
|
||||
}
|
||||
|
||||
extension Toast {
|
||||
func getContextWindows() -> [ToastWindow] {
|
||||
guard let windowScene = windowScene else {
|
||||
return []
|
||||
}
|
||||
return AppContext.toastWindows[windowScene] ?? []
|
||||
}
|
||||
|
||||
static func pop(toast: Toast) {
|
||||
var windows = toast.getContextWindows()
|
||||
guard let window = windows.first(where: { $0.toast == toast }) else {
|
||||
func setContextWindows(_ windows: [ToastWindow]) {
|
||||
guard let windowScene = windowScene else {
|
||||
return
|
||||
}
|
||||
if windows.count > 1 {
|
||||
windows.removeAll { $0 == window }
|
||||
updateToastWindowsLayout(windows: windows)
|
||||
} else if windows.count == 1 {
|
||||
windows.removeAll()
|
||||
} else {
|
||||
consolePrint("‼️代码漏洞:已经没有toast了")
|
||||
}
|
||||
toast.vm.duration = nil
|
||||
toast.setContextWindows(windows)
|
||||
UIView.animateEaseOut(duration: toast.config.animateDurationForBuildOutByDefault) {
|
||||
window.transform = .init(translationX: 0, y: 0-20-window.maxY)
|
||||
} completion: { done in
|
||||
toast.view.removeFromSuperview()
|
||||
toast.removeFromParent()
|
||||
toast.navEvents[.onViewDidDisappear]?(toast)
|
||||
// 这里设置一下window属性,会使window的生命周期被延长到此处,即动画执行过程中window不会被提前释放
|
||||
window.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fileprivate var updateToastsLayoutWorkItem: DispatchWorkItem?
|
||||
|
||||
fileprivate extension ToastWindow {
|
||||
|
||||
static func setToastWindowsLayout(windows: [ToastWindow]) {
|
||||
for (i, window) in windows.enumerated() {
|
||||
let config = window.toast.config
|
||||
var y = window.frame.origin.y
|
||||
if i == 0 {
|
||||
let topLayoutMargins = AppContext.appWindow?.layoutMargins.top ?? config.margin
|
||||
y = max(topLayoutMargins - config.margin, config.margin)
|
||||
} else {
|
||||
if i - 1 < windows.count && i > 0 {
|
||||
y = config.margin + windows[i-1].frame.maxY
|
||||
} else {
|
||||
y = config.margin
|
||||
}
|
||||
}
|
||||
window.maxY = y + window.frame.size.height
|
||||
UIView.animateEaseOut(duration: config.animateDurationForReloadByDefault) {
|
||||
window.frame.origin.y = y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func updateToastWindowsLayout(windows: [ToastWindow]) {
|
||||
updateToastsLayoutWorkItem?.cancel()
|
||||
updateToastsLayoutWorkItem = DispatchWorkItem {
|
||||
setToastWindowsLayout(windows: windows)
|
||||
updateToastsLayoutWorkItem = nil
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now()+0.001, execute: updateToastsLayoutWorkItem!)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fileprivate extension Toast {
|
||||
func calcHeight() -> CGFloat {
|
||||
var height = CGFloat(0)
|
||||
for v in infoStack.arrangedSubviews {
|
||||
// 图片或者文本最大高度
|
||||
height = CGFloat.maximum(v.frame.maxY, height)
|
||||
}
|
||||
if actionStack.arrangedSubviews.count > 0 {
|
||||
height += actionStack.frame.height + contentStack.spacing
|
||||
}
|
||||
contentView.subviews.filter { v in
|
||||
if v == contentMaskView {
|
||||
return false
|
||||
}
|
||||
if v == contentStack {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} .forEach { v in
|
||||
height = CGFloat.maximum(v.frame.maxY, height)
|
||||
}
|
||||
// 上下边间距
|
||||
let cardEdgeInsets = config.cardEdgeInsetsByDefault
|
||||
height += cardEdgeInsets.top + cardEdgeInsets.bottom
|
||||
return height
|
||||
AppContext.toastWindows[windowScene] = windows
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue