SwiftUI Toggle实现乐观UI时避免无限循环的解决方案咨询
解决Toggle网络请求失败无限循环的问题
核心问题
当网络请求失败后重置isOn状态时,会再次触发onChanged回调,导致无限发起网络请求。根源是没有区分用户主动触发的状态变更和代码内部的状态回滚,所有isOn的变化都会触发请求逻辑。
解决方案:使用自定义绑定区分操作来源
通过自定义Toggle的绑定逻辑,让网络请求只在用户主动操作Toggle时触发,ViewModel内部回滚状态时不会触发请求。
修改后的代码
ViewModel部分
@MainActor class ViewModel: ObservableObject { @Published var isOn: Bool = true func updatePreference(isOn: Bool) { Task { var request = URLRequest(...) do { _ = try await URLSession.shared.data(for: request) // 请求成功,无需额外操作,isOn已经是用户设置的值 } catch { // 请求失败,回滚状态(此修改不会触发网络请求) self.isOn = !isOn } } } }
View部分
struct ContentView: View { @ObservedObject var viewModel: ViewModel var body: some View { Toggle(isOn: Binding( get: { viewModel.isOn }, set: { newValue in // 用户操作Toggle时,先更新状态,再发起请求 viewModel.isOn = newValue viewModel.updatePreference(isOn: newValue) } )) { Text("偏好设置") } } }
为什么这个方案有效?
- Toggle的自定义绑定中,
get方法仅负责同步UI状态,set方法只会在用户手动切换Toggle时被调用。 - 当网络请求失败,ViewModel内部修改
isOn回滚状态时,只会触发UI更新,不会调用set方法,因此不会再次发起网络请求,从根源避免了循环。
额外优化:添加加载状态(可选)
如果需要在请求过程中禁用Toggle或显示加载提示,可以在ViewModel中添加加载状态标记:
@MainActor class ViewModel: ObservableObject { @Published var isOn: Bool = true @Published var isLoading = false // 新增加载状态 func updatePreference(isOn: Bool) { guard !isLoading else { return } // 防止重复发起请求 isLoading = true Task { defer { isLoading = false } // 请求结束后重置加载状态 var request = URLRequest(...) do { _ = try await URLSession.shared.data(for: request) } catch { self.isOn = !isOn } } } }
然后在View中禁用Toggle并显示加载:
struct ContentView: View { @ObservedObject var viewModel: ViewModel var body: some View { Toggle(isOn: Binding( get: { viewModel.isOn }, set: { newValue in viewModel.isOn = newValue viewModel.updatePreference(isOn: newValue) } )) { if viewModel.isLoading { ProgressView() } else { Text("偏好设置") } } .disabled(viewModel.isLoading) } }
内容的提问来源于stack exchange,提问作者matt
相关产品推荐
相关产品推荐

