添加 Lottie 全局加载层并修复加载状态卡住问题
This commit is contained in:
@ -12,6 +12,7 @@
|
||||
A1B2C3D12FFB84950033C305 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 93FF17E22FFB84950033C304 /* SnapKit */; };
|
||||
A1B2C3D22FFB84950033C305 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 93FF17E32FFB84950033C304 /* Kingfisher */; };
|
||||
B2C3D4E12FFB84950033C306 /* AlibabaCloudOSS in Frameworks */ = {isa = PBXBuildFile; productRef = B2C3D4E32FFB84950033C306 /* AlibabaCloudOSS */; };
|
||||
C3D4E5F12FFB84950033C307 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = C3D4E5F32FFB84950033C307 /* Lottie */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@ -63,6 +64,7 @@
|
||||
93FF17E42FFB84950033C304 /* SnapKit in Frameworks */,
|
||||
93FF17E52FFB84950033C304 /* Kingfisher in Frameworks */,
|
||||
B2C3D4E12FFB84950033C306 /* AlibabaCloudOSS in Frameworks */,
|
||||
C3D4E5F12FFB84950033C307 /* Lottie in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -119,6 +121,7 @@
|
||||
93FF17E22FFB84950033C304 /* SnapKit */,
|
||||
93FF17E32FFB84950033C304 /* Kingfisher */,
|
||||
B2C3D4E32FFB84950033C306 /* AlibabaCloudOSS */,
|
||||
C3D4E5F32FFB84950033C307 /* Lottie */,
|
||||
);
|
||||
productName = suixinkan;
|
||||
productReference = 93FF17C52FFB84950033C304 /* suixinkan.app */;
|
||||
@ -181,6 +184,7 @@
|
||||
93FF17E02FFB84950033C304 /* XCRemoteSwiftPackageReference "SnapKit" */,
|
||||
93FF17E12FFB84950033C304 /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
||||
B2C3D4E22FFB84950033C306 /* XCRemoteSwiftPackageReference "alibabacloud-oss-swift-sdk-v2" */,
|
||||
C3D4E5F22FFB84950033C307 /* XCRemoteSwiftPackageReference "lottie-spm" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 93FF17C62FFB84950033C304 /* Products */;
|
||||
@ -532,6 +536,14 @@
|
||||
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 */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@ -550,6 +562,11 @@
|
||||
package = B2C3D4E22FFB84950033C306 /* XCRemoteSwiftPackageReference "alibabacloud-oss-swift-sdk-v2" */;
|
||||
productName = AlibabaCloudOSS;
|
||||
};
|
||||
C3D4E5F32FFB84950033C307 /* Lottie */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = C3D4E5F22FFB84950033C307 /* XCRemoteSwiftPackageReference "lottie-spm" */;
|
||||
productName = Lottie;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 93FF17BD2FFB84950033C304 /* Project object */;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "bdf5586d6171744520b6e9c884c2dbe9a16818e8af708876240fda75c94cb2f0",
|
||||
"originHash" : "cb8f20fce1c6d5182e1505245f7fa3cd864954e3b45b7397e3bc7cd403003882",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "alibabacloud-oss-swift-sdk-v2",
|
||||
@ -19,6 +19,15 @@
|
||||
"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",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@ -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>
|
||||
@ -9,9 +9,6 @@ import UIKit
|
||||
/// 业务 ViewController 基类,提供统一的 UI 搭建流程与通用 UI 能力。
|
||||
class BaseViewController: UIViewController {
|
||||
|
||||
private var loadingOverlay: UIView?
|
||||
private var loadingIndicator: UIActivityIndicatorView?
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
@ -47,36 +44,14 @@ class BaseViewController: UIViewController {
|
||||
|
||||
// MARK: - Loading
|
||||
|
||||
@MainActor
|
||||
func showLoading() {
|
||||
if loadingOverlay == nil {
|
||||
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!)
|
||||
GlobalLoadingManager.shared.show()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func hideLoading() {
|
||||
loadingIndicator?.stopAnimating()
|
||||
loadingOverlay?.isHidden = true
|
||||
GlobalLoadingManager.shared.hide()
|
||||
}
|
||||
|
||||
// MARK: - Toast / Alert
|
||||
@ -156,7 +131,7 @@ class BaseViewController: UIViewController {
|
||||
}
|
||||
|
||||
@objc private func handleSessionDidExpireNotification() {
|
||||
hideLoading()
|
||||
GlobalLoadingManager.shared.hideAll()
|
||||
onSessionDidExpire()
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,7 +117,6 @@ final class ProfileViewModel {
|
||||
realNameInfo = nil
|
||||
bankCardInfo = nil
|
||||
}
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func beginEditing() {
|
||||
|
||||
@ -73,7 +73,6 @@ final class StatisticsViewModel {
|
||||
|
||||
do {
|
||||
statistics = try await fetchSummary(range: range, api: api)
|
||||
notifyStateChange()
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
|
||||
1
suixinkan/Resources/Lottie/loading.json
Normal file
1
suixinkan/Resources/Lottie/loading.json
Normal 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":[]}
|
||||
@ -122,8 +122,9 @@ final class HomeViewController: BaseViewController {
|
||||
|
||||
private func initializeHome() async {
|
||||
showLoading()
|
||||
defer { hideLoading() }
|
||||
await viewModel.initialize(api: homeAPI)
|
||||
hideLoading()
|
||||
|
||||
applyViewModel()
|
||||
await evaluateDialogsWithDelay()
|
||||
await viewModel.refreshLocationInfo()
|
||||
|
||||
74
suixinkan/UI/Loading/GlobalLoadingManager.swift
Normal file
74
suixinkan/UI/Loading/GlobalLoadingManager.swift
Normal 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 }
|
||||
}
|
||||
}
|
||||
53
suixinkan/UI/Loading/GlobalLoadingOverlayView.swift
Normal file
53
suixinkan/UI/Loading/GlobalLoadingOverlayView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
51
suixinkan/UI/Loading/LottieLoadingView.swift
Normal file
51
suixinkan/UI/Loading/LottieLoadingView.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
@ -75,7 +75,7 @@ final class ProfileViewController: BaseViewController {
|
||||
|
||||
override func bindActions() {
|
||||
viewModel.onStateChange = { [weak self] in
|
||||
self?.applyViewModel()
|
||||
Task { @MainActor in self?.applyViewModel() }
|
||||
}
|
||||
|
||||
refreshControl.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
|
||||
@ -103,7 +103,7 @@ final class ProfileViewController: BaseViewController {
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
Task { await reloadProfile(showLoading: false) }
|
||||
Task { await reloadProfile(showGlobalLoading: true) }
|
||||
}
|
||||
|
||||
private let headerContainer = UIView()
|
||||
@ -228,12 +228,6 @@ final class ProfileViewController: BaseViewController {
|
||||
value: viewModel.currentScenicName,
|
||||
badge: .scenic
|
||||
)
|
||||
|
||||
if viewModel.isLoading, !refreshControl.isRefreshing {
|
||||
showLoading()
|
||||
} else {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
private func configureWithdrawalRow() {
|
||||
@ -286,13 +280,13 @@ final class ProfileViewController: BaseViewController {
|
||||
|
||||
@objc private func refreshPulled() {
|
||||
Task {
|
||||
await reloadProfile(showLoading: false)
|
||||
await reloadProfile(showGlobalLoading: false)
|
||||
refreshControl.endRefreshing()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleAccountDidSwitch() {
|
||||
Task { await reloadProfile(showLoading: true) }
|
||||
Task { await reloadProfile(showGlobalLoading: true) }
|
||||
}
|
||||
|
||||
@objc private func nicknameChanged() {
|
||||
@ -370,13 +364,19 @@ final class ProfileViewController: BaseViewController {
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func reloadProfile(showLoading: Bool) async {
|
||||
if showLoading { self.showLoading() }
|
||||
private func reloadProfile(showGlobalLoading: Bool) async {
|
||||
let shouldShowOverlay = showGlobalLoading && !refreshControl.isRefreshing
|
||||
if shouldShowOverlay {
|
||||
showLoading()
|
||||
}
|
||||
defer {
|
||||
if showLoading { hideLoading() }
|
||||
if shouldShowOverlay {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
do {
|
||||
try await viewModel.reload(api: profileAPI)
|
||||
applyViewModel()
|
||||
} catch {
|
||||
showToast(error.localizedDescription)
|
||||
}
|
||||
|
||||
@ -91,16 +91,13 @@ final class StatisticsViewController: BaseViewController {
|
||||
}
|
||||
|
||||
private func initializeStatistics() async {
|
||||
showLoading()
|
||||
await viewModel.initStatistics(api: statisticsAPI)
|
||||
hideLoading()
|
||||
applyViewModel()
|
||||
}
|
||||
|
||||
private func selectPeriod(_ period: String) {
|
||||
Task {
|
||||
showLoading()
|
||||
await viewModel.selectPeriod(period, api: statisticsAPI)
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
54
suixinkanTests/GlobalLoadingManagerTests.swift
Normal file
54
suixinkanTests/GlobalLoadingManagerTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user