Files
suixinkan_ios_new/suixinkan/Core/Design/GlobalLoadingCenter.swift
汉秋 63fb0462d6 修复离线路径误弹位置上报超时窗,并优化提醒设置与定位 Loading 展示。
离线或倒计时未进入提醒窗口时不再弹出超时提醒;全局 Loading 支持按需展示定位文案,提前提醒选项抽成共用组件。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 16:22:55 +08:00

241 lines
8.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// GlobalLoadingCenter.swift
// suixinkan
//
// Created by Codex on 2026/6/23.
//
import Combine
import Foundation
import Lottie
import SwiftUI
@MainActor
/// Loading
final class GlobalLoadingCenter {
fileprivate let state = GlobalLoadingState()
/// Loading
func show(message: String = "", showsMessage: Bool = false) {
if !message.isEmpty {
state.message = message
}
state.messageDisplayLevels.append(showsMessage)
state.showsMessage = state.messageDisplayLevels.contains(true)
state.activeCount += 1
state.isVisible = true
}
/// Loading
func hide() {
state.activeCount = max(0, state.activeCount - 1)
if !state.messageDisplayLevels.isEmpty {
state.messageDisplayLevels.removeLast()
}
state.showsMessage = state.messageDisplayLevels.contains(true)
guard state.activeCount == 0 else { return }
state.isVisible = false
state.message = ""
state.messageDisplayLevels = []
state.showsMessage = false
}
/// Loading Loading
func updateMessage(_ message: String) {
guard state.isVisible, !message.isEmpty else { return }
state.message = message
}
/// Loading
func withLoading<T>(
message: String = "",
showsMessage: Bool = false,
operation: () async throws -> T
) async rethrows -> T {
show(message: message, showsMessage: showsMessage)
defer { hide() }
return try await operation()
}
/// Loading loading
func withOptionalLoading<T>(
_ enabled: Bool,
message: String = "",
showsMessage: Bool = false,
operation: () async throws -> T
) async rethrows -> T {
if enabled {
return try await withLoading(message: message, showsMessage: showsMessage, operation: operation)
}
return try await operation()
}
#if DEBUG
/// Loading
var snapshotForTests: GlobalLoadingSnapshot {
GlobalLoadingSnapshot(
isVisible: state.isVisible,
message: state.message,
activeCount: state.activeCount,
showsMessage: state.showsMessage
)
}
#endif
}
@MainActor
/// Loading Overlay
fileprivate final class GlobalLoadingState: ObservableObject {
@Published fileprivate var isVisible = false
@Published fileprivate var message = ""
@Published fileprivate var showsMessage = false
@Published fileprivate var activeCount = 0
fileprivate var messageDisplayLevels: [Bool] = []
}
/// Lottie Loading loading.json
struct LottieLoadingAnimationView: UIViewRepresentable {
/// Loading
static var hasAnimationResource: Bool {
loadAnimation() != nil
}
/// Lottie UIKit
func makeUIView(context: Context) -> UIView {
let container = UIView()
container.backgroundColor = .clear
let animationView = LottieAnimationView()
animationView.translatesAutoresizingMaskIntoConstraints = false
animationView.contentMode = .scaleAspectFit
animationView.loopMode = .loop
animationView.backgroundBehavior = .pauseAndRestore
animationView.animation = Self.loadAnimation()
animationView.play()
container.addSubview(animationView)
NSLayoutConstraint.activate([
animationView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
animationView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
animationView.topAnchor.constraint(equalTo: container.topAnchor),
animationView.bottomAnchor.constraint(equalTo: container.bottomAnchor)
])
return container
}
/// Loading SwiftUI
func updateUIView(_ uiView: UIView, context: Context) {}
/// loading Resources
private static func loadAnimation() -> LottieAnimation? {
if let animation = LottieAnimation.named("loading") {
return animation
}
if let url = Bundle.main.url(forResource: "loading", withExtension: "json", subdirectory: "Resources") {
return LottieAnimation.filepath(url.path)
}
if let url = Bundle.main.url(forResource: "loading", withExtension: "json") {
return LottieAnimation.filepath(url.path)
}
return nil
}
}
/// Loading Overlay 宿 Loading
private struct GlobalLoadingOverlayHost: View {
@ObservedObject private var state: GlobalLoadingState
/// 使 Loading Overlay 宿
init(loadingCenter: GlobalLoadingCenter) {
_state = ObservedObject(wrappedValue: loadingCenter.state)
}
var body: some View {
ZStack {
if state.isVisible {
ZStack {
Color.black.opacity(0.28)
.ignoresSafeArea()
VStack(spacing: 14) {
loadingAnimation
if state.showsMessage, !state.message.isEmpty {
Text(state.message)
.font(.system(size: 15, weight: .medium))
.foregroundStyle(AppDesign.textPrimary)
.multilineTextAlignment(.center)
.padding(.horizontal, 8)
.padding(.bottom, 6)
}
}
.padding(.horizontal, 10)
.padding(.vertical, 10)
.background(.white, in: RoundedRectangle(cornerRadius: 18, style: .continuous))
.shadow(color: Color.black.opacity(0.12), radius: 24, x: 0, y: 10)
}
.transition(.opacity)
.zIndex(10_000)
.accessibilityElement(children: .combine)
.accessibilityLabel(state.showsMessage && !state.message.isEmpty ? state.message : "加载中")
}
}
.animation(.easeInOut(duration: 0.2), value: state.isVisible)
.animation(.easeInOut(duration: 0.2), value: state.showsMessage)
.animation(.easeInOut(duration: 0.2), value: state.message)
}
@ViewBuilder
private var loadingAnimation: some View {
if LottieLoadingAnimationView.hasAnimationResource {
LottieLoadingAnimationView()
.frame(width: 132, height: 132)
} else {
ProgressView()
.controlSize(.large)
.tint(AppDesign.primary)
.frame(width: 132, height: 132)
}
}
}
private struct GlobalLoadingOverlayModifier: ViewModifier {
let loadingCenter: GlobalLoadingCenter
/// Loading
func body(content: Content) -> some View {
ZStack {
content
GlobalLoadingOverlayHost(loadingCenter: loadingCenter)
}
}
}
extension View {
/// Loading RootView
func globalLoadingOverlay(loadingCenter: GlobalLoadingCenter) -> some View {
modifier(GlobalLoadingOverlayModifier(loadingCenter: loadingCenter))
}
}
#if DEBUG
/// Loading
struct GlobalLoadingSnapshot: Equatable {
let isVisible: Bool
let message: String
let activeCount: Int
let showsMessage: Bool
}
#endif
private struct GlobalLoadingCenterKey: EnvironmentKey {
static let defaultValue = GlobalLoadingCenter()
}
extension EnvironmentValues {
/// Loading show/hide
var globalLoading: GlobalLoadingCenter {
get { self[GlobalLoadingCenterKey.self] }
set { self[GlobalLoadingCenterKey.self] = newValue }
}
}