新增Capsule组件

This commit is contained in:
xaoxuu 2023-08-17 15:56:58 +08:00
parent 4ffd6c5617
commit 3ede7ea236
29 changed files with 1174 additions and 342 deletions

View File

@ -7,6 +7,7 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* 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 */; }; CD6537BF28C3311B00A5981B /* ListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6537BE28C3311B00A5981B /* ListModel.swift */; };
CD6537C128C35E1C00A5981B /* ListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6537C028C35E1C00A5981B /* ListVC.swift */; }; CD6537C128C35E1C00A5981B /* ListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6537C028C35E1C00A5981B /* ListVC.swift */; };
CD6537C328C35E6200A5981B /* ToastVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6537C228C35E6200A5981B /* ToastVC.swift */; }; CD6537C328C35E6200A5981B /* ToastVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6537C228C35E6200A5981B /* ToastVC.swift */; };
@ -26,6 +27,7 @@
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference 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>"; }; 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>"; }; 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>"; }; CD6537C228C35E6200A5981B /* ToastVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastVC.swift; sourceTree = "<group>"; };
@ -86,6 +88,7 @@
CD6537C228C35E6200A5981B /* ToastVC.swift */, CD6537C228C35E6200A5981B /* ToastVC.swift */,
CDB7A1CF28C32A7400E034D8 /* AlertVC.swift */, CDB7A1CF28C32A7400E034D8 /* AlertVC.swift */,
CD6537C428C35F2C00A5981B /* SheetVC.swift */, CD6537C428C35F2C00A5981B /* SheetVC.swift */,
CD2439332A82164E00A3BBF5 /* CapsuleVC.swift */,
CD8EEF4028BC5C7200E660EA /* Main.storyboard */, CD8EEF4028BC5C7200E660EA /* Main.storyboard */,
CD8EEF4328BC5C7300E660EA /* Assets.xcassets */, CD8EEF4328BC5C7300E660EA /* Assets.xcassets */,
CD8EEF4528BC5C7300E660EA /* LaunchScreen.storyboard */, CD8EEF4528BC5C7300E660EA /* LaunchScreen.storyboard */,
@ -184,6 +187,7 @@
files = ( files = (
CDA83DB928C601E60025F0DF /* TableHeaderView.swift in Sources */, CDA83DB928C601E60025F0DF /* TableHeaderView.swift in Sources */,
CD6537C528C35F2C00A5981B /* SheetVC.swift in Sources */, CD6537C528C35F2C00A5981B /* SheetVC.swift in Sources */,
CD2439342A82164E00A3BBF5 /* CapsuleVC.swift in Sources */,
CD6537C328C35E6200A5981B /* ToastVC.swift in Sources */, CD6537C328C35E6200A5981B /* ToastVC.swift in Sources */,
CDB7A1D028C32A7400E034D8 /* AlertVC.swift in Sources */, CDB7A1D028C32A7400E034D8 /* AlertVC.swift in Sources */,
CD6537C128C35E1C00A5981B /* ListVC.swift in Sources */, CD6537C128C35E1C00A5981B /* ListVC.swift in Sources */,

View File

@ -15,7 +15,7 @@ class AlertVC: ListVC {
// Uncomment the following line to preserve selection between presentations // Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false // self.clearsSelectionOnViewWillAppear = false
header.titleLabel.text = "ProHUD.Alert" title = "Alert"
header.detailLabel.text = "弹窗控件,用于强阻塞性交互,用户必须做出选择或者等待结果才能进入下一步,当多个实例出现时,会以堆叠的形式显示,新的实例会在覆盖旧的实例上层。" header.detailLabel.text = "弹窗控件,用于强阻塞性交互,用户必须做出选择或者等待结果才能进入下一步,当多个实例出现时,会以堆叠的形式显示,新的实例会在覆盖旧的实例上层。"
Alert.Configuration.shared { config in Alert.Configuration.shared { config in

View File

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <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="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
<!--Alert--> <!--AlertVC-->
<scene sceneID="LP0-RE-kvY"> <scene sceneID="LP0-RE-kvY">
<objects> <objects>
<tableViewController id="NBo-Re-tKO" customClass="AlertVC" customModule="PHDemo" customModuleProvider="target" sceneMemberID="viewController"> <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"/> <outlet property="delegate" destination="NBo-Re-tKO" id="1L3-SV-7FG"/>
</connections> </connections>
</tableView> </tableView>
<tabBarItem key="tabBarItem" title="Alert" image="exclamationmark.triangle.fill" catalog="system" id="pLJ-z8-SS1"/> <navigationItem key="navigationItem" id="vza-Sb-cyH"/>
</tableViewController> </tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="ydp-D5-Zdx" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="ydp-D5-Zdx" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="1865" y="804"/> <point key="canvasLocation" x="3684.057971014493" y="803.57142857142856"/>
</scene> </scene>
<!--Tab Bar Controller--> <!--Tab Bar Controller-->
<scene sceneID="ej2-I3-4Bd"> <scene sceneID="ej2-I3-4Bd">
@ -50,16 +50,17 @@
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tabBar> </tabBar>
<connections> <connections>
<segue destination="h7R-Kd-Dn5" kind="relationship" relationship="viewControllers" id="4rA-b5-Kd6"/> <segue destination="N1b-1U-hgP" kind="relationship" relationship="viewControllers" id="4rA-b5-Kd6"/>
<segue destination="NBo-Re-tKO" kind="relationship" relationship="viewControllers" id="4BA-vv-RD2"/> <segue destination="XXi-nT-rRc" kind="relationship" relationship="viewControllers" id="4BA-vv-RD2"/>
<segue destination="9SY-ag-pK6" kind="relationship" relationship="viewControllers" id="vVd-PW-h6L"/> <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> </connections>
</tabBarController> </tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="avc-BE-wZC" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="avc-BE-wZC" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="1865.217391304348" y="71.651785714285708"/> <point key="canvasLocation" x="1865.217391304348" y="71.651785714285708"/>
</scene> </scene>
<!--Toast--> <!--ToastVC-->
<scene sceneID="DAh-i5-GcF"> <scene sceneID="DAh-i5-GcF">
<objects> <objects>
<tableViewController id="h7R-Kd-Dn5" customClass="ToastVC" customModule="PHDemo" customModuleProvider="target" sceneMemberID="viewController"> <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"/> <outlet property="delegate" destination="h7R-Kd-Dn5" id="8Px-ei-ipU"/>
</connections> </connections>
</tableView> </tableView>
<tabBarItem key="tabBarItem" title="Toast" image="bubble.left.fill" catalog="system" id="YYs-U3-EWo"/> <navigationItem key="navigationItem" id="CTm-r7-VWj"/>
</tableViewController> </tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="VmT-Tm-s4d" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="VmT-Tm-s4d" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="954" y="804"/> <point key="canvasLocation" x="1863.7681159420292" y="803.57142857142856"/>
</scene> </scene>
<!--Sheet--> <!--SheetVC-->
<scene sceneID="DG9-RE-7gC"> <scene sceneID="DG9-RE-7gC">
<objects> <objects>
<tableViewController id="9SY-ag-pK6" customClass="SheetVC" customModule="PHDemo" customModuleProvider="target" sceneMemberID="viewController"> <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"/> <outlet property="delegate" destination="9SY-ag-pK6" id="UkD-l4-OhM"/>
</connections> </connections>
</tableView> </tableView>
<tabBarItem key="tabBarItem" title="Sheet" image="iphone" catalog="system" id="3or-OI-jbb"/> <navigationItem key="navigationItem" id="Mf8-vT-Rhv"/>
</tableViewController> </tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Pal-Bf-SfP" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="Pal-Bf-SfP" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects> </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> </scene>
</scenes> </scenes>
<resources> <resources>
<image name="bubble.left.fill" catalog="system" width="128" height="110"/> <image name="capsule.inset.filled" catalog="system" width="128" height="96"/>
<image name="exclamationmark.triangle.fill" catalog="system" width="128" height="109"/> <image name="exclamationmark.circle.fill" catalog="system" width="128" height="123"/>
<image name="iphone" catalog="system" width="112" height="128"/> <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"> <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> </systemColor>
</resources> </resources>
</document> </document>

View File

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

View File

@ -12,7 +12,7 @@ class ListVC: UITableViewController {
var list = ListModel() var list = ListModel()
lazy var header: TableHeaderView = TableHeaderView(text: "ProHUD") lazy var header: TableHeaderView = TableHeaderView()
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@ -21,9 +21,25 @@ class ListVC: UITableViewController {
tableView.sectionHeaderHeight = 32 tableView.sectionHeaderHeight = 32
tableView.sectionFooterHeight = 8 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 { override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections // #warning Incomplete implementation, return the number of sections
list.sections.count list.sections.count

View File

@ -13,7 +13,7 @@ class SheetVC: ListVC {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
header.titleLabel.text = "ProHUD.Sheet" title = "Sheet"
header.detailLabel.text = "操作表控件,用于弱阻塞性交互。显示区域为从屏幕底部向上弹出的新图层,可以放置丰富的内容,自由度较高。" header.detailLabel.text = "操作表控件,用于弱阻塞性交互。显示区域为从屏幕底部向上弹出的新图层,可以放置丰富的内容,自由度较高。"
list.add(title: "默认布局") { section in list.add(title: "默认布局") { section in

View File

@ -9,16 +9,8 @@ import UIKit
class TableHeaderView: UIView { 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 = { 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.font = .systemFont(ofSize: 15, weight: .regular)
lb.textAlignment = .justified lb.textAlignment = .justified
lb.numberOfLines = 0 lb.numberOfLines = 0
@ -26,23 +18,18 @@ class TableHeaderView: UIView {
return lb return lb
}() }()
convenience init(text: String) { convenience init() {
self.init(frame: .init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 150)) self.init(frame: .init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 80))
titleLabel.text = text
} }
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
addSubview(detailLabel) 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 detailLabel.snp.makeConstraints { make in
make.left.right.equalTo(titleLabel) make.left.right.equalToSuperview().inset(24)
make.top.equalTo(titleLabel.snp.bottom).offset(12) make.top.equalToSuperview().offset(8)
} }
} }

View File

@ -47,9 +47,11 @@ class ToastVC: ListVC {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
title = "Toast"
let title = "通知条控件" let title = "通知条控件"
let message = "通知条控件,用于非阻塞性事件通知。显示效果如同原生通知,默认会自动消失,可以支持手势移除,有多条通知可以平铺并列显示。" let message = "通知条控件,用于非阻塞性事件通知。显示效果如同原生通知,默认会自动消失,可以支持手势移除,有多条通知可以平铺并列显示。"
header.titleLabel.text = "ProHUD.Toast"
header.detailLabel.text = message header.detailLabel.text = message
Toast.Configuration.shared { config in Toast.Configuration.shared { config in

View File

@ -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() super.init()
if let vm = vm { self.vm = vm
self.vm = vm
}
handler?(self) handler?(self)
DispatchQueue.main.async { DispatchQueue.main.async {
if handler != nil { if handler != nil {
@ -97,7 +95,7 @@ open class Alert: ProHUD.Controller {
} }
@discardableResult public convenience init(handler: ((_ alert: Alert) -> Void)?) { @discardableResult public convenience init(handler: ((_ alert: Alert) -> Void)?) {
self.init(nil, handler: handler) self.init(.init(), handler: handler)
} }
public override func viewDidLoad() { public override func viewDidLoad() {

View File

@ -68,7 +68,7 @@ extension Alert: DefaultLayout {
if contentView.superview != view { if contentView.superview != view {
view.insertSubview(contentView, at: 0) view.insertSubview(contentView, at: 0)
} }
let alerts = window?.alerts ?? [] let alerts = attachedWindow?.alerts ?? []
if config.enableShadow && alerts.count > 0 { if config.enableShadow && alerts.count > 0 {
contentView.clipsToBounds = false contentView.clipsToBounds = false
contentView.layer.shadowRadius = 4 contentView.layer.shadowRadius = 4
@ -195,7 +195,11 @@ extension Alert {
if bodyCount > 0 { if bodyCount > 0 {
config.customTitleLabel?(titleLabel) config.customTitleLabel?(titleLabel)
} else { } else {
config.customTextLabel?(titleLabel) if let customTextLabel = config.customTextLabel {
customTextLabel(titleLabel)
} else {
titleLabel.font = .boldSystemFont(ofSize: 18)
}
} }
} else { } else {
if textStack.arrangedSubviews.contains(titleLabel) { if textStack.arrangedSubviews.contains(titleLabel) {
@ -211,7 +215,11 @@ extension Alert {
if titleCount > 0 { if titleCount > 0 {
config.customBodyLabel?(bodyLabel) config.customBodyLabel?(bodyLabel)
} else { } else {
config.customTextLabel?(bodyLabel) if let customTextLabel = config.customTextLabel {
customTextLabel(bodyLabel)
} else {
bodyLabel.font = .boldSystemFont(ofSize: 18)
}
} }
} else { } else {
if textStack.arrangedSubviews.contains(bodyLabel) { if textStack.arrangedSubviews.contains(bodyLabel) {
@ -246,7 +254,7 @@ extension Alert {
public override func viewDidLayoutSubviews() { public override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews() super.viewDidLayoutSubviews()
let alerts = window?.alerts ?? [] let alerts = attachedWindow?.alerts ?? []
if config.enableShadow && alerts.count > 1 { if config.enableShadow && alerts.count > 1 {
contentView.layer.shadowPath = UIBezierPath.init(rect: contentView.bounds).cgPath contentView.layer.shadowPath = UIBezierPath.init(rect: contentView.bounds).cgPath
} }

View File

@ -39,7 +39,6 @@ extension Alert: HUD {
@objc open func pop() { @objc open func pop() {
navEvents[.onViewWillDisappear]?(self) navEvents[.onViewWillDisappear]?(self)
let window = window ?? createAttachedWindowIfNotExists()
Alert.removeAlert(alert: self) Alert.removeAlert(alert: self)
let duration = config.animateDurationForBuildOut ?? config.animateDurationForBuildOutByDefault let duration = config.animateDurationForBuildOut ?? config.animateDurationForBuildOutByDefault
UIView.animateEaseOut(duration: duration) { UIView.animateEaseOut(duration: duration) {
@ -51,9 +50,10 @@ extension Alert: HUD {
self.navEvents[.onViewDidDisappear]?(self) self.navEvents[.onViewDidDisappear]?(self)
} }
// hide window // hide window
guard let window = view.window as? AlertWindow, let windowScene = windowScene else { return }
let count = window.alerts.count let count = window.alerts.count
if count == 0 { if count == 0 {
self.window = nil AppContext.alertWindow[windowScene] = nil
UIView.animateEaseOut(duration: duration) { UIView.animateEaseOut(duration: duration) {
window.backgroundView.alpha = 0 window.backgroundView.alpha = 0
} completion: { done in } completion: { done in
@ -106,6 +106,8 @@ public extension Alert {
} }
// MARK: - layout
fileprivate extension Alert { fileprivate extension Alert {
static func updateAlertsLayout(alerts: [Alert]) { static func updateAlertsLayout(alerts: [Alert]) {
for (i, a) in alerts.reversed().enumerated() { for (i, a) in alerts.reversed().enumerated() {
@ -124,7 +126,7 @@ fileprivate extension Alert {
} }
static func removeAlert(alert: Alert) { static func removeAlert(alert: Alert) {
guard var alerts = alert.window?.alerts else { guard var alerts = alert.attachedWindow?.alerts else {
return return
} }
if alerts.count > 1 { if alerts.count > 1 {
@ -141,7 +143,7 @@ fileprivate extension Alert {
} else { } else {
print("代码漏洞已经没有alert了") print("代码漏洞已经没有alert了")
} }
alert.window?.alerts = alerts alert.attachedWindow?.alerts = alerts
} }
} }

View File

@ -7,23 +7,6 @@
import UIKit 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 { class AlertWindow: Window {
var alerts: [Alert] = [] var alerts: [Alert] = []
@ -45,8 +28,14 @@ class AlertWindow: Window {
AppContext.alertWindow[windowScene] = w AppContext.alertWindow[windowScene] = w
} }
// alert // alert
w.windowLevel = .init(rawValue: UIWindow.Level.alert.rawValue - 1) w.windowLevel = .phAlert
return w return w
} }
} }
extension Alert {
var attachedWindow: AlertWindow? {
view.window as? AlertWindow
}
}

View File

@ -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
}()
/// imageViewtextLabel)
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)
}
}

View File

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

View File

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

View File

@ -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.sizewindow.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?()
// }
// }
//
//}

View File

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

View File

@ -106,13 +106,11 @@ public class Configuration: NSObject {
// MARK: // MARK:
var customTextLabel: ((_ label: UILabel) -> Void)? = { label in var customTextLabel: ((_ label: UILabel) -> Void)?
label.font = .boldSystemFont(ofSize: 18)
}
/// ///
/// - Parameter handler: /// - Parameter handler:
public func customTextLabel(handler: @escaping (_ label: UILabel) -> Void) { public func customTextLabel(_ handler: @escaping (_ label: UILabel) -> Void) {
customTextLabel = handler customTextLabel = handler
} }
@ -120,7 +118,7 @@ public class Configuration: NSObject {
/// ///
/// - Parameter handler: /// - Parameter handler:
public func customTitleLabel(handler: @escaping (_ label: UILabel) -> Void) { public func customTitleLabel(_ handler: @escaping (_ label: UILabel) -> Void) {
customTitleLabel = handler customTitleLabel = handler
} }
@ -129,7 +127,7 @@ public class Configuration: NSObject {
/// ///
/// - Parameter handler: /// - Parameter handler:
public func customBodyLabel(handler: @escaping (_ label: UILabel) -> Void) { public func customBodyLabel(_ handler: @escaping (_ label: UILabel) -> Void) {
customBodyLabel = handler customBodyLabel = handler
} }

View File

@ -30,6 +30,10 @@ open class ViewModel: NSObject {
} }
} }
public required override init() {
}
public convenience init(icon: UIImage? = nil, duration: TimeInterval? = nil) { public convenience init(icon: UIImage? = nil, duration: TimeInterval? = nil) {
self.init() self.init()
self.icon = icon self.icon = icon
@ -62,83 +66,95 @@ open class ViewModel: NSObject {
// MARK: - convenience func // MARK: - convenience func
public extension ViewModel { public extension ViewModel {
func icon(_ image: UIImage?) -> ViewModel { func icon(_ image: UIImage?) -> Self {
self.icon = image self.icon = image
return self return self
} }
func icon(_ imageURL: URL?) -> ViewModel { func icon(_ imageURL: URL?) -> Self {
self.iconURL = imageURL self.iconURL = imageURL
return self return self
} }
func title(_ text: String?) -> Self {
func title(_ text: String?) -> ViewModel {
self.title = text self.title = text
return self return self
} }
func message(_ text: String?) -> ViewModel { func message(_ text: String?) -> Self {
self.message = text self.message = text
return self return self
} }
func duration(_ seconds: TimeInterval?) -> ViewModel { func duration(_ seconds: TimeInterval?) -> Self {
self.duration = seconds self.duration = seconds
return self return self
} }
func rotation(_ rotation: Rotation?) -> Self {
self.rotation = rotation
return self
}
} }
// MARK: - example scenes // MARK: - example scenes
public extension ViewModel { public extension ViewModel {
// MARK: plain // MARK: plain
static func title(_ text: String?) -> ViewModel { static func title(_ text: String?) -> Self {
let obj = ViewModel() .init()
obj.title = text .title(text)
return obj
} }
static func message(_ text: String?) -> ViewModel { static func message(_ text: String?) -> Self {
let obj = ViewModel() .init()
obj.message = text .message(text)
return obj
} }
// MARK: loading // MARK: loading
static var loading: ViewModel { static var loading: Self {
let obj = ViewModel(icon: UIImage(inProHUD: "prohud.windmill")) .init()
obj.rotation = .init(repeatCount: .infinity) .icon(.init(inProHUD: "prohud.windmill"))
return obj .rotation(.init(repeatCount: .infinity))
} }
static func loading(_ seconds: TimeInterval) -> ViewModel { static func loading(_ seconds: TimeInterval) -> Self {
let obj = ViewModel(icon: UIImage(inProHUD: "prohud.windmill"), duration: seconds) .init()
obj.rotation = .init(repeatCount: .infinity) .icon(.init(inProHUD: "prohud.windmill"))
return obj .rotation(.init(repeatCount: .infinity))
.duration(seconds)
} }
// MARK: success // MARK: success
static var success: ViewModel { static var success: Self {
.init(icon: UIImage(inProHUD: "prohud.checkmark")) .init()
.icon(.init(inProHUD: "prohud.checkmark"))
} }
static func success(_ seconds: TimeInterval) -> ViewModel { static func success(_ seconds: TimeInterval) -> Self {
.init(icon: UIImage(inProHUD: "prohud.checkmark"), duration: seconds) .init()
.icon(.init(inProHUD: "prohud.checkmark"))
.duration(seconds)
} }
// MARK: warning // MARK: warning
static var warning: ViewModel { static var warning: Self {
.init(icon: UIImage(inProHUD: "prohud.exclamationmark")) .init()
.icon(.init(inProHUD: "prohud.exclamationmark"))
} }
static func warning(_ seconds: TimeInterval) -> ViewModel { static func warning(_ seconds: TimeInterval) -> Self {
.init(icon: UIImage(inProHUD: "prohud.exclamationmark"), duration: seconds) .init()
.icon(.init(inProHUD: "prohud.exclamationmark"))
.duration(seconds)
} }
// MARK: error // MARK: error
static var error: ViewModel { static var error: Self {
.init(icon: UIImage(inProHUD: "prohud.xmark")) .init()
.icon(.init(inProHUD: "prohud.xmark"))
} }
static func error(_ seconds: TimeInterval) -> ViewModel { static func error(_ seconds: TimeInterval) -> Self {
.init(icon: UIImage(inProHUD: "prohud.xmark"), duration: seconds) .init()
.icon(.init(inProHUD: "prohud.xmark"))
.duration(seconds)
} }
// MARK: failure // MARK: failure
static var failure: ViewModel { error } static var failure: Self { error }
static func failure(_ seconds: TimeInterval) -> ViewModel { error(seconds) } static func failure(_ seconds: TimeInterval) -> Self { error(seconds) }
} }

View File

@ -36,6 +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: [Capsule.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 }
@ -116,10 +117,11 @@ extension AppContext {
var sheetWindows: [SheetWindow] { var sheetWindows: [SheetWindow] {
Self.sheetWindows[windowScene] ?? [] Self.sheetWindows[windowScene] ?? []
} }
}
extension AppContext {
var toastWindows: [ToastWindow] { var toastWindows: [ToastWindow] {
Self.toastWindows[windowScene] ?? [] Self.toastWindows[windowScene] ?? []
} }
var capsuleWindows: [Capsule.CapsuleViewModel.Position: CapsuleWindow] {
Self.capsuleWindows[windowScene] ?? [:]
}
} }

View File

@ -30,3 +30,21 @@ var isPortrait: Bool {
var isPhonePortrait: Bool { var isPhonePortrait: Bool {
UIDevice.current.userInterfaceIdiom == .phone && (AppContext.windowScene?.interfaceOrientation.isPortrait == true) 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)
}

View File

@ -10,7 +10,7 @@ import UIKit
extension UIView { extension UIView {
static func animateEaseOut(duration: TimeInterval, animations: @escaping () -> Void, completion: ((_ done: Bool) -> Void)? = nil) { 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)
} }
} }

View File

@ -48,7 +48,7 @@ open class Sheet: Controller {
handler(self) handler(self)
DispatchQueue.main.async { DispatchQueue.main.async {
SheetWindow.push(sheet: self) self.push()
} }
} }
@ -80,7 +80,7 @@ extension Sheet {
if let act = onTappedBackground { if let act = onTappedBackground {
act(self) act(self)
} else { } else {
SheetWindow.pop(sheet: self) self.pop()
} }
} }
} }

View File

@ -11,11 +11,50 @@ extension Sheet: HUD {
@objc open func push() { @objc open func push() {
guard Configuration.isEnabled else { return } 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() { @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)
}
} }
} }

View File

@ -7,22 +7,6 @@
import UIKit 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 { class SheetWindow: Window {
var sheet: Sheet var sheet: Sheet
@ -35,7 +19,7 @@ class SheetWindow: Window {
super.init(frame: AppContext.appBounds) super.init(frame: AppContext.appBounds)
} }
sheet.window = self sheet.window = self
windowLevel = .init(rawValue: UIWindow.Level.alert.rawValue - 2) windowLevel = .phSheet
isHidden = false isHidden = false
} }
@ -43,52 +27,19 @@ class SheetWindow: Window {
fatalError("init(coder:) has not been implemented") 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()
}
}
static func pop(sheet: Sheet) { extension Sheet {
var windows = sheet.getContextWindows() func getContextWindows() -> [SheetWindow] {
guard let window = windows.first(where: { $0.sheet == sheet }) else { guard let windowScene = windowScene else {
return []
}
return AppContext.sheetWindows[windowScene] ?? []
}
func setContextWindows(_ windows: [SheetWindow]) {
guard let windowScene = windowScene else {
return return
} }
sheet.navEvents[.onViewWillDisappear]?(sheet) AppContext.sheetWindows[windowScene] = windows
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)
}
}
} }
} }

View File

@ -19,7 +19,7 @@ open class Toast: Controller {
public var progressView: ProgressView? public var progressView: ProgressView?
/// icontextStackactionStack) /// infoStackactionStack)
public lazy var contentStack: StackView = { public lazy var contentStack: StackView = {
let stack = StackView(axis: .vertical) let stack = StackView(axis: .vertical)
stack.spacing = 16 stack.spacing = 16
@ -27,7 +27,7 @@ open class Toast: Controller {
return stack return stack
}() }()
/// image+text /// imageView+textStack
public lazy var infoStack: StackView = { public lazy var infoStack: StackView = {
let stack = StackView(axis: .horizontal) let stack = StackView(axis: .horizontal)
stack.spacing = 8 stack.spacing = 8
@ -36,7 +36,7 @@ open class Toast: Controller {
return stack return stack
}() }()
/// /// titlebody
public lazy var textStack: StackView = { public lazy var textStack: StackView = {
let stack = StackView(axis: .vertical) let stack = StackView(axis: .vertical)
stack.spacing = config.lineSpace 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() super.init()
if let vm = vm { self.vm = vm
self.vm = vm
}
handler?(self) handler?(self)
DispatchQueue.main.async { DispatchQueue.main.async {
if handler != nil { if handler != nil {
ToastWindow.push(toast: self) self.push()
} }
} }
} }
@discardableResult public convenience init(handler: ((_ toast: Toast) -> Void)?) { @discardableResult public convenience init(handler: ((_ toast: Toast) -> Void)?) {
self.init(nil, handler: handler) self.init(.init(), handler: handler)
} }
required public init?(coder: NSCoder) { required public init?(coder: NSCoder) {

View File

@ -48,7 +48,11 @@ extension Toast: DefaultLayout {
if bodyCount > 0 { if bodyCount > 0 {
config.customTitleLabel?(titleLabel) config.customTitleLabel?(titleLabel)
} else { } else {
config.customTextLabel?(bodyLabel) if let customTextLabel = config.customTextLabel {
customTextLabel(titleLabel)
} else {
titleLabel.font = .boldSystemFont(ofSize: 18)
}
} }
} else { } else {
if textStack.arrangedSubviews.contains(titleLabel) { if textStack.arrangedSubviews.contains(titleLabel) {
@ -61,7 +65,11 @@ extension Toast: DefaultLayout {
if titleCount > 0 { if titleCount > 0 {
config.customBodyLabel?(bodyLabel) config.customBodyLabel?(bodyLabel)
} else { } else {
config.customTextLabel?(bodyLabel) if let customTextLabel = config.customTextLabel {
customTextLabel(bodyLabel)
} else {
bodyLabel.font = .boldSystemFont(ofSize: 18)
}
} }
} else { } else {
if textStack.arrangedSubviews.contains(bodyLabel) { if textStack.arrangedSubviews.contains(bodyLabel) {

View File

@ -9,13 +9,99 @@ import UIKit
extension Toast: HUD { 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() { @objc open func push() {
guard Configuration.isEnabled else { return } 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.sizewindow.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() { @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使windowwindow
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!)
}
}

View File

@ -7,21 +7,6 @@
import UIKit 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 { class ToastWindow: Window {
var toast: Toast var toast: Toast
@ -33,7 +18,7 @@ class ToastWindow: Window {
super.init(frame: .zero) super.init(frame: .zero)
windowScene = AppContext.windowScene windowScene = AppContext.windowScene
toast.window = self toast.window = self
windowLevel = .init(rawValue: UIWindow.Level.alert.rawValue + 1000) windowLevel = .phToast
layer.shadowRadius = 8 layer.shadowRadius = 8
layer.shadowOffset = .init(width: 0, height: 5) layer.shadowOffset = .init(width: 0, height: 5)
layer.shadowOpacity = 0.2 layer.shadowOpacity = 0.2
@ -49,139 +34,19 @@ class ToastWindow: Window {
layer.shadowPath = UIBezierPath.init(rect: bounds).cgPath 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 extension Toast {
let cardEdgeInsets = config.cardEdgeInsetsByDefault func getContextWindows() -> [ToastWindow] {
let width = CGFloat.minimum(AppContext.appBounds.width - config.windowEdgeInsetByDefault - config.windowEdgeInsetByDefault, config.cardMaxWidthByDefault) guard let windowScene = windowScene else {
toast.view.frame.size = CGSize(width: width, height: config.cardMaxHeightByDefault) return []
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.sizewindow.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)
} }
return AppContext.toastWindows[windowScene] ?? []
} }
func setContextWindows(_ windows: [ToastWindow]) {
static func pop(toast: Toast) { guard let windowScene = windowScene else {
var windows = toast.getContextWindows()
guard let window = windows.first(where: { $0.toast == toast }) else {
return return
} }
if windows.count > 1 { AppContext.toastWindows[windowScene] = windows
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使windowwindow
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
} }
} }