添加 Lottie 全局加载层并修复加载状态卡住问题

This commit is contained in:
2026-07-06 17:43:13 +08:00
parent d79d3003e3
commit 71ab38af8d
14 changed files with 281 additions and 75 deletions

View File

@ -12,6 +12,7 @@
A1B2C3D12FFB84950033C305 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 93FF17E22FFB84950033C304 /* SnapKit */; }; A1B2C3D12FFB84950033C305 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 93FF17E22FFB84950033C304 /* SnapKit */; };
A1B2C3D22FFB84950033C305 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 93FF17E32FFB84950033C304 /* Kingfisher */; }; A1B2C3D22FFB84950033C305 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 93FF17E32FFB84950033C304 /* Kingfisher */; };
B2C3D4E12FFB84950033C306 /* AlibabaCloudOSS in Frameworks */ = {isa = PBXBuildFile; productRef = B2C3D4E32FFB84950033C306 /* AlibabaCloudOSS */; }; B2C3D4E12FFB84950033C306 /* AlibabaCloudOSS in Frameworks */ = {isa = PBXBuildFile; productRef = B2C3D4E32FFB84950033C306 /* AlibabaCloudOSS */; };
C3D4E5F12FFB84950033C307 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = C3D4E5F32FFB84950033C307 /* Lottie */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -63,6 +64,7 @@
93FF17E42FFB84950033C304 /* SnapKit in Frameworks */, 93FF17E42FFB84950033C304 /* SnapKit in Frameworks */,
93FF17E52FFB84950033C304 /* Kingfisher in Frameworks */, 93FF17E52FFB84950033C304 /* Kingfisher in Frameworks */,
B2C3D4E12FFB84950033C306 /* AlibabaCloudOSS in Frameworks */, B2C3D4E12FFB84950033C306 /* AlibabaCloudOSS in Frameworks */,
C3D4E5F12FFB84950033C307 /* Lottie in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -119,6 +121,7 @@
93FF17E22FFB84950033C304 /* SnapKit */, 93FF17E22FFB84950033C304 /* SnapKit */,
93FF17E32FFB84950033C304 /* Kingfisher */, 93FF17E32FFB84950033C304 /* Kingfisher */,
B2C3D4E32FFB84950033C306 /* AlibabaCloudOSS */, B2C3D4E32FFB84950033C306 /* AlibabaCloudOSS */,
C3D4E5F32FFB84950033C307 /* Lottie */,
); );
productName = suixinkan; productName = suixinkan;
productReference = 93FF17C52FFB84950033C304 /* suixinkan.app */; productReference = 93FF17C52FFB84950033C304 /* suixinkan.app */;
@ -181,6 +184,7 @@
93FF17E02FFB84950033C304 /* XCRemoteSwiftPackageReference "SnapKit" */, 93FF17E02FFB84950033C304 /* XCRemoteSwiftPackageReference "SnapKit" */,
93FF17E12FFB84950033C304 /* XCRemoteSwiftPackageReference "Kingfisher" */, 93FF17E12FFB84950033C304 /* XCRemoteSwiftPackageReference "Kingfisher" */,
B2C3D4E22FFB84950033C306 /* XCRemoteSwiftPackageReference "alibabacloud-oss-swift-sdk-v2" */, B2C3D4E22FFB84950033C306 /* XCRemoteSwiftPackageReference "alibabacloud-oss-swift-sdk-v2" */,
C3D4E5F22FFB84950033C307 /* XCRemoteSwiftPackageReference "lottie-spm" */,
); );
preferredProjectObjectVersion = 77; preferredProjectObjectVersion = 77;
productRefGroup = 93FF17C62FFB84950033C304 /* Products */; productRefGroup = 93FF17C62FFB84950033C304 /* Products */;
@ -532,6 +536,14 @@
minimumVersion = 0.3.0; minimumVersion = 0.3.0;
}; };
}; };
C3D4E5F22FFB84950033C307 /* XCRemoteSwiftPackageReference "lottie-spm" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/airbnb/lottie-spm.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 4.6.1;
};
};
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
@ -550,6 +562,11 @@
package = B2C3D4E22FFB84950033C306 /* XCRemoteSwiftPackageReference "alibabacloud-oss-swift-sdk-v2" */; package = B2C3D4E22FFB84950033C306 /* XCRemoteSwiftPackageReference "alibabacloud-oss-swift-sdk-v2" */;
productName = AlibabaCloudOSS; productName = AlibabaCloudOSS;
}; };
C3D4E5F32FFB84950033C307 /* Lottie */ = {
isa = XCSwiftPackageProductDependency;
package = C3D4E5F22FFB84950033C307 /* XCRemoteSwiftPackageReference "lottie-spm" */;
productName = Lottie;
};
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
}; };
rootObject = 93FF17BD2FFB84950033C304 /* Project object */; rootObject = 93FF17BD2FFB84950033C304 /* Project object */;

View File

@ -1,5 +1,5 @@
{ {
"originHash" : "bdf5586d6171744520b6e9c884c2dbe9a16818e8af708876240fda75c94cb2f0", "originHash" : "cb8f20fce1c6d5182e1505245f7fa3cd864954e3b45b7397e3bc7cd403003882",
"pins" : [ "pins" : [
{ {
"identity" : "alibabacloud-oss-swift-sdk-v2", "identity" : "alibabacloud-oss-swift-sdk-v2",
@ -19,6 +19,15 @@
"version" : "8.10.0" "version" : "8.10.0"
} }
}, },
{
"identity" : "lottie-spm",
"kind" : "remoteSourceControl",
"location" : "https://github.com/airbnb/lottie-spm.git",
"state" : {
"revision" : "0aff16288f8dc1e3dab35cbf5cf45979673fd3df",
"version" : "4.6.1"
}
},
{ {
"identity" : "snapkit", "identity" : "snapkit",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@ -9,9 +9,6 @@ import UIKit
/// ViewController UI UI /// ViewController UI UI
class BaseViewController: UIViewController { class BaseViewController: UIViewController {
private var loadingOverlay: UIView?
private var loadingIndicator: UIActivityIndicatorView?
// MARK: - Lifecycle // MARK: - Lifecycle
override func viewDidLoad() { override func viewDidLoad() {
@ -47,36 +44,14 @@ class BaseViewController: UIViewController {
// MARK: - Loading // MARK: - Loading
@MainActor
func showLoading() { func showLoading() {
if loadingOverlay == nil { GlobalLoadingManager.shared.show()
let overlay = UIView()
overlay.backgroundColor = UIColor.black.withAlphaComponent(0.1)
let indicator = UIActivityIndicatorView(style: .large)
indicator.hidesWhenStopped = true
view.addSubview(overlay)
overlay.addSubview(indicator)
overlay.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
indicator.snp.makeConstraints { make in
make.center.equalToSuperview()
}
loadingOverlay = overlay
loadingIndicator = indicator
}
loadingOverlay?.isHidden = false
loadingIndicator?.startAnimating()
view.bringSubviewToFront(loadingOverlay!)
} }
@MainActor
func hideLoading() { func hideLoading() {
loadingIndicator?.stopAnimating() GlobalLoadingManager.shared.hide()
loadingOverlay?.isHidden = true
} }
// MARK: - Toast / Alert // MARK: - Toast / Alert
@ -156,7 +131,7 @@ class BaseViewController: UIViewController {
} }
@objc private func handleSessionDidExpireNotification() { @objc private func handleSessionDidExpireNotification() {
hideLoading() GlobalLoadingManager.shared.hideAll()
onSessionDidExpire() onSessionDidExpire()
} }
} }

View File

@ -117,7 +117,6 @@ final class ProfileViewModel {
realNameInfo = nil realNameInfo = nil
bankCardInfo = nil bankCardInfo = nil
} }
notifyStateChange()
} }
func beginEditing() { func beginEditing() {

View File

@ -73,7 +73,6 @@ final class StatisticsViewModel {
do { do {
statistics = try await fetchSummary(range: range, api: api) statistics = try await fetchSummary(range: range, api: api)
notifyStateChange()
} catch is CancellationError { } catch is CancellationError {
return return
} catch { } catch {

View File

@ -0,0 +1 @@
{"v":"4.8.0","meta":{"g":"LottieFiles AE 1.0.0","a":"","k":"","d":"","tc":"#FFFFFF"},"fr":25,"ip":25,"op":55,"w":800,"h":800,"nm":"Loading #18","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Line","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.453],"y":[1]},"o":{"x":[0.538],"y":[0]},"t":0,"s":[20]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.538],"y":[0]},"t":15,"s":[100]},{"i":{"x":[0.453],"y":[1]},"o":{"x":[0.538],"y":[0]},"t":30,"s":[20]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.538],"y":[0]},"t":45,"s":[100]},{"t":60,"s":[20]}],"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[400,400,0],"ix":2},"a":{"a":0,"k":[-175,46,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-2,0]],"o":[[0,0],[2,0]],"v":[[-268,46],[-82,46]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.2745,0.3608,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":39,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.453],"y":[1]},"o":{"x":[0.538],"y":[0]},"t":0,"s":[45]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.538],"y":[0]},"t":15,"s":[0]},{"i":{"x":[0.453],"y":[1]},"o":{"x":[0.538],"y":[0]},"t":30,"s":[45]},{"i":{"x":[0.453],"y":[1]},"o":{"x":[0.538],"y":[0]},"t":45,"s":[0]},{"t":60,"s":[45]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.453],"y":[1]},"o":{"x":[0.538],"y":[0]},"t":0,"s":[55]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.538],"y":[0]},"t":15,"s":[100]},{"i":{"x":[0.453],"y":[1]},"o":{"x":[0.538],"y":[0]},"t":30,"s":[55]},{"i":{"x":[0.453],"y":[1]},"o":{"x":[0.538],"y":[0]},"t":45,"s":[100]},{"t":60,"s":[55]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":125,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Line 5","parent":3,"refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,400,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":25,"op":80,"st":25,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"Line 4","parent":3,"refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[470,400,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":22,"op":77,"st":22,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"Line 3","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[400,400,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":19,"op":74,"st":19,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"Line 2","parent":3,"refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[330,400,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":16,"op":71,"st":16,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"Line 1","parent":3,"refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[260,400,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":13,"op":68,"st":13,"bm":0}],"markers":[]}

View File

@ -122,8 +122,9 @@ final class HomeViewController: BaseViewController {
private func initializeHome() async { private func initializeHome() async {
showLoading() showLoading()
defer { hideLoading() }
await viewModel.initialize(api: homeAPI) await viewModel.initialize(api: homeAPI)
hideLoading()
applyViewModel() applyViewModel()
await evaluateDialogsWithDelay() await evaluateDialogsWithDelay()
await viewModel.refreshLocationInfo() await viewModel.refreshLocationInfo()

View File

@ -0,0 +1,74 @@
//
// GlobalLoadingManager.swift
// suixinkan
//
import UIKit
/// Loading Android `GlobalLoadingManager`
@MainActor
final class GlobalLoadingManager {
static let shared = GlobalLoadingManager()
private var loadingCount = 0
private weak var hostWindow: UIWindow?
private var overlayView: GlobalLoadingOverlayView?
private init() {}
/// Loading
var isShowing: Bool {
loadingCount > 0
}
/// Loading
func show() {
loadingCount += 1
guard loadingCount == 1 else { return }
attachOverlayIfNeeded()
overlayView?.present()
}
/// Loading
func hide() {
guard loadingCount > 0 else { return }
loadingCount -= 1
guard loadingCount == 0 else { return }
overlayView?.dismiss()
overlayView?.removeFromSuperview()
}
/// Loading
func hideAll() {
loadingCount = 0
overlayView?.dismiss()
overlayView?.removeFromSuperview()
}
private func attachOverlayIfNeeded() {
guard let window = keyWindow else { return }
hostWindow = window
if overlayView == nil {
let overlay = GlobalLoadingOverlayView(frame: window.bounds)
overlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
overlay.isHidden = true
overlayView = overlay
}
guard let overlayView else { return }
if overlayView.superview !== window {
overlayView.removeFromSuperview()
window.addSubview(overlayView)
}
window.bringSubviewToFront(overlayView)
}
private var keyWindow: UIWindow? {
UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap(\.windows)
.first { $0.isKeyWindow }
}
}

View File

@ -0,0 +1,53 @@
//
// GlobalLoadingOverlayView.swift
// suixinkan
//
import SnapKit
import UIKit
/// Loading Android `LoadingScrim`
final class GlobalLoadingOverlayView: UIView {
private let cardView = UIView()
private let loadingView = LottieLoadingView()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.black.withAlphaComponent(0.2)
isUserInteractionEnabled = true
cardView.backgroundColor = .white
cardView.layer.cornerRadius = 15
cardView.clipsToBounds = true
addSubview(cardView)
cardView.addSubview(loadingView)
cardView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(100)
}
loadingView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(80)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func present() {
isHidden = false
loadingView.startAnimating()
}
///
func dismiss() {
loadingView.stopAnimating()
isHidden = true
}
}

View File

@ -0,0 +1,51 @@
//
// LottieLoadingView.swift
// suixinkan
//
import Lottie
import SnapKit
import UIKit
/// Lottie Loading 使 `Resources/Lottie/loading.json`
final class LottieLoadingView: UIView {
private let animationView: LottieAnimationView
override init(frame: CGRect) {
animationView = LottieLoadingView.makeAnimationView()
super.init(frame: frame)
addSubview(animationView)
animationView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func startAnimating() {
animationView.loopMode = .loop
animationView.play()
}
///
func stopAnimating() {
animationView.stop()
}
private static func makeAnimationView() -> LottieAnimationView {
if let url = loadingAnimationURL() {
return LottieAnimationView(filePath: url.path)
}
return LottieAnimationView()
}
private static func loadingAnimationURL() -> URL? {
Bundle.main.url(forResource: "loading", withExtension: "json", subdirectory: "Resources/Lottie")
?? Bundle.main.url(forResource: "loading", withExtension: "json")
}
}

View File

@ -75,7 +75,7 @@ final class ProfileViewController: BaseViewController {
override func bindActions() { override func bindActions() {
viewModel.onStateChange = { [weak self] in viewModel.onStateChange = { [weak self] in
self?.applyViewModel() Task { @MainActor in self?.applyViewModel() }
} }
refreshControl.addTarget(self, action: #selector(refreshPulled), for: .valueChanged) refreshControl.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
@ -103,7 +103,7 @@ final class ProfileViewController: BaseViewController {
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
Task { await reloadProfile(showLoading: false) } Task { await reloadProfile(showGlobalLoading: true) }
} }
private let headerContainer = UIView() private let headerContainer = UIView()
@ -228,12 +228,6 @@ final class ProfileViewController: BaseViewController {
value: viewModel.currentScenicName, value: viewModel.currentScenicName,
badge: .scenic badge: .scenic
) )
if viewModel.isLoading, !refreshControl.isRefreshing {
showLoading()
} else {
hideLoading()
}
} }
private func configureWithdrawalRow() { private func configureWithdrawalRow() {
@ -286,13 +280,13 @@ final class ProfileViewController: BaseViewController {
@objc private func refreshPulled() { @objc private func refreshPulled() {
Task { Task {
await reloadProfile(showLoading: false) await reloadProfile(showGlobalLoading: false)
refreshControl.endRefreshing() refreshControl.endRefreshing()
} }
} }
@objc private func handleAccountDidSwitch() { @objc private func handleAccountDidSwitch() {
Task { await reloadProfile(showLoading: true) } Task { await reloadProfile(showGlobalLoading: true) }
} }
@objc private func nicknameChanged() { @objc private func nicknameChanged() {
@ -370,13 +364,19 @@ final class ProfileViewController: BaseViewController {
present(alert, animated: true) present(alert, animated: true)
} }
private func reloadProfile(showLoading: Bool) async { private func reloadProfile(showGlobalLoading: Bool) async {
if showLoading { self.showLoading() } let shouldShowOverlay = showGlobalLoading && !refreshControl.isRefreshing
if shouldShowOverlay {
showLoading()
}
defer { defer {
if showLoading { hideLoading() } if shouldShowOverlay {
hideLoading()
}
} }
do { do {
try await viewModel.reload(api: profileAPI) try await viewModel.reload(api: profileAPI)
applyViewModel()
} catch { } catch {
showToast(error.localizedDescription) showToast(error.localizedDescription)
} }

View File

@ -91,16 +91,13 @@ final class StatisticsViewController: BaseViewController {
} }
private func initializeStatistics() async { private func initializeStatistics() async {
showLoading()
await viewModel.initStatistics(api: statisticsAPI) await viewModel.initStatistics(api: statisticsAPI)
hideLoading() applyViewModel()
} }
private func selectPeriod(_ period: String) { private func selectPeriod(_ period: String) {
Task { Task {
showLoading()
await viewModel.selectPeriod(period, api: statisticsAPI) await viewModel.selectPeriod(period, api: statisticsAPI)
hideLoading()
} }
} }

View File

@ -0,0 +1,54 @@
//
// GlobalLoadingManagerTests.swift
// suixinkanTests
//
import XCTest
@testable import suixinkan
@MainActor
/// Loading
final class GlobalLoadingManagerTests: XCTestCase {
override func tearDown() {
GlobalLoadingManager.shared.hideAll()
super.tearDown()
}
func testShowAndHideToggleVisibilityState() {
let manager = GlobalLoadingManager.shared
XCTAssertFalse(manager.isShowing)
manager.show()
XCTAssertTrue(manager.isShowing)
manager.hide()
XCTAssertFalse(manager.isShowing)
}
func testNestedShowRequiresMatchingHideCount() {
let manager = GlobalLoadingManager.shared
manager.show()
manager.show()
XCTAssertTrue(manager.isShowing)
manager.hide()
XCTAssertTrue(manager.isShowing)
manager.hide()
XCTAssertFalse(manager.isShowing)
}
func testHideAllResetsNestedCount() {
let manager = GlobalLoadingManager.shared
manager.show()
manager.show()
manager.hideAll()
XCTAssertFalse(manager.isShowing)
manager.hide()
XCTAssertFalse(manager.isShowing)
}
}