SwiftUI异步请求返回前View被释放,如何保证一致性?
解决SwiftUI导航中ViewModel重复初始化与生命周期问题
核心问题分析
SwiftUI的navigationDestination闭包会因预渲染、状态变化等原因被多次调用,导致FormView及其内部的FormViewModel被重复初始化。当网络请求返回时,旧的FormViewModel已被释放,无法更新UI数据。
解决方案
1. 使用@StateObject管理ViewModel生命周期
将UserFormView中的@ObservedObject替换为@StateObject,@StateObject会确保ViewModel仅初始化一次,且在View的整个生命周期内保持实例稳定,即使View被SwiftUI重建也不会销毁ViewModel。
修改后的UserFormView代码:
struct UserFormView: View { // 用@StateObject替代@ObservedObject,保证ViewModel单例且生命周期与View绑定 @StateObject private var viewModel = FormViewModel() @State var first: String = "" @State var last: String = "" @State var department: Department? = nil var id: Int? var body: some View { VStack { HStack { TextField("First", text: $first) .textFieldStyle(RoundedBorderTextFieldStyle()) .font(.system(size: 16)) TextField("Last", text: $last) .textFieldStyle(RoundedBorderTextFieldStyle()) .font(.system(size: 16)) } HStack { Picker(selection: $department, label: Text("")) { ForEach(viewModel.departments, id: \.self) { department in Text(department.name ?? "").tag(department) } } .pickerStyle(WheelPickerStyle()) .padding() } } .padding() .navigationTitle("User Form") .onAppear { viewModel.findAllDepartments() } } }
2. 避免重复发起网络请求
在FormViewModel的findAllDepartments方法中添加判断,仅当部门列表为空时才发起请求,防止多次调用onAppear导致重复请求。
修改后的FormViewModel代码:
class FormViewModel: ObservableObject { @Published var user: User? = nil @Published var departments: [Department] = [] private var cancellable = Set<AnyCancellable>() private var rand = Int(arc4random_uniform(10)) + 1 init() { print("init \(rand)") } func findAllDepartments() { // 已加载过部门列表则直接返回,避免重复请求 guard departments.isEmpty else { return } delegate?.findAllDepartments() .receive(on: DispatchQueue.main) .sink(receiveCompletion: {[weak self] completion in switch completion{ case .finished: print("finished") case .failure(let error): print(error) } }, receiveValue: { [weak self] departments in self?.departments.append(contentsOf: departments) // 无需手动发送objectWillChange,@Published会自动触发 // self?.objectWillChange.send() }) .store(in: &cancellable) } deinit { print("deinit \(rand)") } }
3. 可选:将ViewModel提升至父View管理(多实例场景)
如果需要为不同用户ID维护独立的ViewModel实例,可以在UserListView中用字典存储ViewModel,通过参数传递给FormView,确保每个ID对应唯一的ViewModel:
修改后的UserListView代码示例:
struct UserListView: View { @ObservedObject private var viewModel = ViewModel() // 为每个用户ID存储对应的FormViewModel @StateObject private var formViewModels = [Int: FormViewModel]() var body: some View { NavigationStack { List { ForEach(viewModel.users) { user in NavigationLink(value: user.id) { Text(user.givenName) } } } .navigationTitle("UserList") .navigationDestination(for: Int.self, destination: { id in // 懒加载或创建对应ID的ViewModel let vm = formViewModels[id] ?? FormViewModel() formViewModels[id] = vm UserFormView(id: id, viewModel: vm) }) .onAppear { viewModel.findAll() } } } } // 修改UserFormView接收外部ViewModel struct UserFormView: View { @ObservedObject var viewModel: FormViewModel @State var first: String = "" @State var last: String = "" @State var department: Department? = nil var id: Int? // ... 其余body代码不变 }
关键说明
@StateObject是SwiftUI 2.0+引入的属性包装器,专门用于管理ObservableObject的生命周期,确保View重建时ViewModel不被销毁。- 手动判断请求状态避免重复网络调用,减少不必要的资源消耗和数据不一致问题。
- 当需要为多个实例维护独立ViewModel时,父View集中管理实例是更可靠的方式。
内容的提问来源于stack exchange,提问作者Source
相关产品推荐
相关产品推荐

