mirror of https://github.com/xaoxuu/ProHUD
新增Capsule组件
This commit is contained in:
parent
4ffd6c5617
commit
3ede7ea236
|
@ -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 */,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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()
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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: 文本样式
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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] ?? [:]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ open class Toast: Controller {
|
||||||
|
|
||||||
public var progressView: ProgressView?
|
public var progressView: ProgressView?
|
||||||
|
|
||||||
/// 内容容器(包括icon、textStack、actionStack)
|
/// 内容容器(包括infoStack、actionStack)
|
||||||
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
|
||||||
}()
|
}()
|
||||||
|
|
||||||
/// 文本容器
|
/// 文本容器(title、body)
|
||||||
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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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.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() {
|
@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
|
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
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func pop(toast: Toast) {
|
extension Toast {
|
||||||
var windows = toast.getContextWindows()
|
func getContextWindows() -> [ToastWindow] {
|
||||||
guard let window = windows.first(where: { $0.toast == toast }) else {
|
guard let windowScene = windowScene else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return AppContext.toastWindows[windowScene] ?? []
|
||||||
|
}
|
||||||
|
func setContextWindows(_ windows: [ToastWindow]) {
|
||||||
|
guard let windowScene = windowScene 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属性,会使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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue