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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 13:35:17