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
相关产品推荐
相关产品推荐

