Swift中异步数据返回后切换页面的更优实现方案咨询
优化后的登录异步跳转实现方案
核心优化思路
原实现的主要问题在于全局状态耦合、职责划分模糊,以及导航控制不够直观。资深开发者通常会遵循MVVM架构,将状态管理、API请求、视图展示解耦,同时优先使用更简洁的Swift并发语法,完善错误与加载状态处理。
具体代码实现
1. 定义数据模型与API层(职责单一)
API层仅负责发起网络请求并返回结果,不参与状态管理,便于测试和复用:
// 登录结果模型,根据实际API返回字段定义 struct LoginResult: Decodable { let userId: String let token: String // 其他业务字段... } // 独立API层:仅处理网络请求逻辑 class AuthAPI { private let externalAPI: SomeExternalAPI // 依赖注入,方便单元测试替换模拟API init(externalAPI: SomeExternalAPI = SomeExternalAPI()) { self.externalAPI = externalAPI } // 用Swift Async/Await封装请求(iOS 15+支持) func login() async throws -> LoginResult { // 如果外部API是Combine类型,可转成Async/Await: // return try await externalAPI.login().values.first! return try await externalAPI.login() } }
2. ViewModel管理状态与业务逻辑
ViewModel作为视图与API的中间层,统一处理登录逻辑、状态更新和错误捕获:
import SwiftUI @MainActor // 确保状态更新在主线程执行 class AuthViewModel: ObservableObject { @Published var isLoggingIn = false // 加载状态 @Published var loginError: Error? // 错误信息 @Published var loginResult: LoginResult? // 登录成功数据 private let authAPI: AuthAPI init(authAPI: AuthAPI = AuthAPI()) { self.authAPI = authAPI } func performLogin() async { isLoggingIn = true loginError = nil do { loginResult = try await authAPI.login() // 登录成功后的附加逻辑:比如保存token到Keychain } catch { loginError = error } isLoggingIn = false } }
3. 视图层实现(iOS 16+ 用NavigationStack)
用NavigationStack的路径绑定控制跳转,同时展示加载状态与错误提示:
struct HomeView: View { @StateObject private var viewModel = AuthViewModel() @State private var navigationPath = NavigationPath() var body: some View { NavigationStack(path: $navigationPath) { VStack(spacing: 20) { // 登录按钮:加载中禁用,显示进度圈 Button("登录") { Task { await viewModel.performLogin() // 登录成功后触发跳转 if viewModel.loginResult != nil { navigationPath.append("userHome") } } } .disabled(viewModel.isLoggingIn) .overlay { if viewModel.isLoggingIn { ProgressView() } } // 错误提示文本 if let error = viewModel.loginError { Text("登录失败:\(error.localizedDescription)") .foregroundColor(.red) .font(.caption) } } .padding() .navigationDestination(for: String.self) { route in if route == "userHome" { UserHomeView(loginResult: viewModel.loginResult!) } } } } } // 登录成功后的页面 struct UserHomeView: View { let loginResult: LoginResult var body: some View { VStack { Text("欢迎回来!") .font(.title) Text("用户ID:\(loginResult.userId)") .font(.subheadline) } .navigationTitle("个人主页") } }
兼容iOS 15及以下(Combine实现)
如果需要支持旧系统,用Combine实现ViewModel,同时保持解耦:
class AuthViewModel: ObservableObject { @Published var isLoggingIn = false @Published var loginError: Error? @Published var loginResult: LoginResult? private let authAPI: AuthAPI private var cancellables = Set<AnyCancellable>() init(authAPI: AuthAPI = AuthAPI()) { self.authAPI = authAPI } func performLogin() { isLoggingIn = true loginError = nil authAPI.loginPublisher() // 假设返回AnyPublisher<LoginResult, Error> .receive(on: DispatchQueue.main) .sink(receiveCompletion: { [weak self] completion in guard let self = self else { return } self.isLoggingIn = false if case .failure(let error) = completion { self.loginError = error } }, receiveValue: { [weak self] result in guard let self = self else { return } self.loginResult = result }) .store(in: &cancellables) } } // 视图层调整 struct HomeView: View { @StateObject private var viewModel = AuthViewModel() @State private var isLoggedIn = false var body: some View { NavigationView { VStack(spacing: 20) { Button("登录") { viewModel.performLogin() } .disabled(viewModel.isLoggingIn) .overlay { if viewModel.isLoggingIn { ProgressView() } } if let error = viewModel.loginError { Text("登录失败:\(error.localizedDescription)") .foregroundColor(.red) } NavigationLink( destination: UserHomeView(loginResult: viewModel.loginResult!), isActive: $isLoggedIn, label: { EmptyView() } ) } .onChange(of: viewModel.loginResult) { isLoggedIn = $0 != nil } } } }
优化要点总结
- 解耦设计:API层、ViewModel、视图各司其职,避免全局状态耦合,提升可维护性
- 可测试性:依赖注入模式,方便单元测试时替换模拟API
- 用户体验:完善加载状态、错误提示,防止重复点击
- 语法简洁:优先使用Async/Await替代Combine,代码可读性更高
- 导航灵活:用
NavigationStack路径导航替代旧版isActive绑定,逻辑更清晰
内容的提问来源于stack exchange,提问作者AaronC
相关产品推荐
相关产品推荐

