You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI中如何在网络层全局控制加载器的显示与隐藏?

统一管理全局加载状态的实现方案

你可以通过以下两种方式,在全局APIService中统一控制ContentView的加载状态,无需在每个ViewModel中单独管理:


方案一:基于NotificationCenter实现(适配现有代码)

这种方式和你现有处理token过期的逻辑一致,无需大幅重构代码:

1. 修改APIService,添加请求计数与通知发送

添加请求计数器(避免并发请求时提前隐藏加载器),在请求开始/结束时发送通知:

class APIService {
    static let shared = APIService()
    private let timeInterval = 60
    // 线程安全的请求计数器
    private var activeRequestsCount = 0
    private let queue = DispatchQueue(label: "com.yourapp.apirequest.count")
    
    // 定义加载状态通知名称
    static let willStartRequest = Notification.Name("APIService.willStartRequest")
    static let didFinishRequest = Notification.Name("APIService.didFinishRequest")
    
    func makeApiTypeRequest<T: Codable>(
        url: String,
        param: [String: Any]? = nil,
        methodType: HttpMethodType,
        expecting: T.Type,
        passToken: Bool = true,
        completion: @escaping (Result<T, Error>)->Void) {
            
            guard let url = URL(string: url) else {
                return
            }
            
            // 请求开始:计数+1,发送"开始加载"通知
            queue.sync {
                activeRequestsCount += 1
                if activeRequestsCount == 1 {
                    DispatchQueue.main.async {
                        NotificationCenter.default.post(name: APIService.willStartRequest, object: nil)
                    }
                }
            }
            
            var request = URLRequest(url: url, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: TimeInterval(timeInterval))
            if let param = param {
                let finalData = try? JSONSerialization.data(withJSONObject: param)
                request.httpBody = finalData
            }
            
            request.httpMethod = methodType.rawValue
            request.addValue("application/json", forHTTPHeaderField: "content-type")
        
            
            if passToken {
                let data = KeychainHelper.standard.read(service: Constant.tokenKey)
                if let data = data {
                    let accessToken = String(data: data, encoding: .utf8)
                    if let accessToken, accessToken != "" {
                        let headers = ["Content-Type": "application/json", "Authorization": "Bearer " + accessToken]
                        request.allHTTPHeaderFields = headers
                    }
                }
            }
        
            URLSession.shared.dataTask(with: request) { [weak self] (data,response,error) in
                defer {
                    // 请求结束:计数-1,发送"结束加载"通知
                    self?.queue.sync {
                        self?.activeRequestsCount -= 1
                        if self?.activeRequestsCount == 0 {
                            DispatchQueue.main.async {
                                NotificationCenter.default.post(name: APIService.didFinishRequest, object: nil)
                            }
                        }
                    }
                }
                
                do {
                    if let error = error {
                        completion(.failure(error))
                        return
                    }
                    
                    if let data = data {
                        if let httpStatus = response as? HTTPURLResponse {
                            switch httpStatus.statusCode {
                            case 400:
                                completion(.failure(CustomError.forbidden))
                            case 200:
                                let respObj = try JSONDecoder().decode(T.self, from: data)
                                completion(.success(respObj))
                            case 500:
                                DispatchQueue.main.async {
                                    NotificationCenter.default.post(name: Notification.Name("test.token.expired"), object: nil)
                                }
                                completion(.failure(CustomError.tokenExpired))
                            default:
                                completion(.failure(CustomError.unknown))
                            }
                        }
                        
                    } else {
                        completion(.failure(CustomError.unknown))
                        print("no data found")
                    }
                } catch(let error) {
                    completion(.failure(CustomError.unknown))
                    print("Error A123 \(error.localizedDescription)")
                }
            }.resume()
    }
}

2. 修改ContentView,监听加载状态通知

添加对加载通知的监听,更新isLoading状态:

struct ContentView: View {
    @EnvironmentObject var session: SessionManager
    @State var showMenu: Bool = false
    @State var isLoading: Bool = false
    @State var isAppInstalled: Bool = false
    
    let tokenExpiredPub = NotificationCenter.default
                .publisher(for: NSNotification.Name("test.token.expired"))
    // 加载状态通知监听
    let willStartRequestPub = NotificationCenter.default
                .publisher(for: APIService.willStartRequest)
    let didFinishRequestPub = NotificationCenter.default
                .publisher(for: APIService.didFinishRequest)
    
    var body: some View {
        NavigationView {
            ZStack {
                if isAppInstalled {
                    if session.currentUserState == .loggedIn {
                        GeometryReader { geometry in
                            TabContainer(showMenu: $showMenu)
                                .frame(width: geometry.size.width, height: geometry.size.height)
                            if showMenu {
                                MenuView()
                                    .frame(width: geometry.size.width/2)
                            }
                        }
                        
                    } else {
                        LoginView(showMenu: $showMenu)
                    }
                    
                    if isLoading {
                        Loading()
                            .ignoresSafeArea()
                    }
                } else {
                    GettingStartedView(isInstalled: $isAppInstalled)
                }
            }
            .onAppear {
                if GettingStartedPersistance.shared.isAppAlreadyInstalled() {
                    isAppInstalled = true
                }
            }
            .onReceive(tokenExpiredPub) { _ in
                session.signout()
            }
            // 请求开始时显示加载
            .onReceive(willStartRequestPub) { _ in
                isLoading = true
            }
            // 所有请求结束时隐藏加载
            .onReceive(didFinishRequestPub) { _ in
                isLoading = false
            }
        }
    }
}

方案二:基于ObservableObject的全局状态(SwiftUI推荐方案)

这种方式更符合SwiftUI的状态管理理念,代码逻辑更清晰:

1. 创建全局LoadingManager

class LoadingManager: ObservableObject {
    static let shared = LoadingManager()
    @Published var isLoading: Bool = false
    private var activeRequestsCount = 0
    private let queue = DispatchQueue(label: "com.yourapp.loading.count")
    
    // 更新请求计数并同步加载状态
    func incrementRequestCount() {
        queue.sync {
            activeRequestsCount += 1
            DispatchQueue.main.async {
                self.isLoading = self.activeRequestsCount > 0
            }
        }
    }
    
    func decrementRequestCount() {
        queue.sync {
            activeRequestsCount -= 1
            DispatchQueue.main.async {
                self.isLoading = self.activeRequestsCount > 0
            }
        }
    }
}

2. 修改APIService,使用LoadingManager

class APIService {
    static let shared = APIService()
    private let timeInterval = 60
    private let loadingManager = LoadingManager.shared
    
    func makeApiTypeRequest<T: Codable>(
        url: String,
        param: [String: Any]? = nil,
        methodType: HttpMethodType,
        expecting: T.Type,
        passToken: Bool = true,
        completion: @escaping (Result<T, Error>)->Void) {
            
            guard let url = URL(string: url) else {
                return
            }
            
            // 请求开始,更新加载状态
            loadingManager.incrementRequestCount()
            
            var request = URLRequest(url: url, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: TimeInterval(timeInterval))
            if let param = param {
                let finalData = try? JSONSerialization.data(withJSONObject: param)
                request.httpBody = finalData
            }
            
            request.httpMethod = methodType.rawValue
            request.addValue("application/json", forHTTPHeaderField: "content-type")
        
            
            if passToken {
                let data = KeychainHelper.standard.read(service: Constant.tokenKey)
                if let data = data {
                    let accessToken = String(data: data, encoding: .utf8)
                    if let accessToken, accessToken != "" {
                        let headers = ["Content-Type": "application/json", "Authorization": "Bearer " + accessToken]
                        request.allHTTPHeaderFields = headers
                    }
                }
            }
        
            URLSession.shared.dataTask(with: request) { [weak self] (data,response,error) in
                defer {
                    // 请求结束,更新加载状态
                    self?.loadingManager.decrementRequestCount()
                }
                
                do {
                    if let error = error {
                        completion(.failure(error))
                        return
                    }
                    
                    if let data = data {
                        if let httpStatus = response as? HTTPURLResponse {
                            switch httpStatus.statusCode {
                            case 400:
                                completion(.failure(CustomError.forbidden))
                            case 200:
                                let respObj = try JSONDecoder().decode(T.self, from: data)
                                completion(.success(respObj))
                            case 500:
                                DispatchQueue.main.async {
                                    NotificationCenter.default.post(name: Notification.Name("test.token.expired"), object: nil)
                                }
                                completion(.failure(CustomError.tokenExpired))
                            default:
                                completion(.failure(CustomError.unknown))
                            }
                        }
                        
                    } else {
                        completion(.failure(CustomError.unknown))
                        print("no data found")
                    }
                } catch(let error) {
                    completion(.failure(CustomError.unknown))
                    print("Error A123 \(error.localizedDescription)")
                }
            }.resume()
    }
}

3. 修改ContentView,注入并监听LoadingManager

首先在App入口注入全局状态:

@main
struct YourApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(SessionManager())
                .environmentObject(LoadingManager.shared) // 注入LoadingManager
        }
    }
}

然后修改ContentView:

struct ContentView: View {
    @EnvironmentObject var session: SessionManager
    @EnvironmentObject var loadingManager: LoadingManager // 接收全局加载状态
    @State var showMenu: Bool = false
    @State var isAppInstalled: Bool = false
    
    let tokenExpiredPub = NotificationCenter.default
                .publisher(for: NSNotification.Name("test.token.expired"))
    
    var body: some View {
        NavigationView {
            ZStack {
                if isAppInstalled {
                    if session.currentUserState == .loggedIn {
                        GeometryReader { geometry in
                            TabContainer(showMenu: $showMenu)
                                .frame(width: geometry.size.width, height: geometry.size.height)
                            if showMenu {
                                MenuView()
                                    .frame(width: geometry.size.width/2)
                            }
                        }
                        
                    } else {
                        LoginView(showMenu: $showMenu)
                    }
                    
                    // 直接使用全局加载状态
                    if loadingManager.isLoading {
                        Loading()
                            .ignoresSafeArea()
                    }
                } else {
                    GettingStartedView(isInstalled: $isAppInstalled)
                }
            }
            .onAppear {
                if GettingStartedPersistance.shared.isAppAlreadyInstalled() {
                    isAppInstalled = true
                }
            }
            .onReceive(tokenExpiredPub) { _ in
                session.signout()
            }
        }
    }
}

内容的提问来源于stack exchange,提问作者user10469417

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 09:05:20