SwiftUI异步网络请求后@State值不更新,无法更新DetailView数据
问题分析
你的核心问题在于:League是值类型(struct),当你在ViewModel里更新leagues数组中某个League的seasons时,DetailView里的@State var league是独立的副本,不会自动同步ViewModel中数组里的最新数据。另外,ChangeButton切换联赛时,虽然通过Binding更新了DetailView的league,但这个新的league同样是ViewModel数组里的副本,后续ViewModel更新该league的seasons时,DetailView的副本还是不会同步。
解决方案
我们需要让DetailView直接依赖ViewModel中的数据源,而不是维护自己的@State副本。具体修改如下:
1. 修改DetailView,用联赛ID追踪选中项
移除@State var league,改用@State var selectedLeagueId,通过ID从ViewModel的数组中实时获取最新的联赛数据:
struct DetailView: View { @EnvironmentObject var vm: LeagueViewModel @State var selectedLeagueId: String // 实时从ViewModel获取当前选中的联赛 private var currentLeague: League? { vm.leagues.first(where: { $0.id == selectedLeagueId }) } var body: some View { VStack { if let league = currentLeague { if let unwrappedSeasons = league.seasons { List { ForEach(unwrappedSeasons, id: \.year) { season in Text(season.displayName) } } } else { ProgressView() } } else { Text("联赛不存在") } } .onAppear { Task { try await vm.loadSeasons(forLeague: selectedLeagueId) } } .navigationBarTitle(Text(currentLeague?.name ?? "联赛详情"), displayMode: .inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { ChangeButton(selectedLeagueId: $selectedLeagueId) } } } }
2. 修改ChangeButton,绑定联赛ID
不再绑定League实例,而是绑定ID,切换时直接更新ID,让DetailView自动获取最新数据:
struct ChangeButton: View { @EnvironmentObject var vm: LeagueViewModel @Binding var selectedLeagueId: String var body: some View { Menu { ForEach(vm.leagues) { league in Button { self.selectedLeagueId = league.id } label: { Text(league.name) } } } label: { Image(systemName: "calendar") } } }
3. 更新ContentView的导航链接传递ID
把原来传递League实例改成传递ID:
struct ContentView: View { @StateObject var vm = LeagueViewModel() var body: some View { NavigationView { VStack { if vm.leagues.isEmpty { ProgressView() } else { List { ForEach(vm.leagues) { league in NavigationLink(destination: DetailView(selectedLeagueId: league.id)) { Text(league.name) } } } } } .navigationBarTitle(Text("联赛列表"), displayMode: .large) } .environmentObject(vm) } }
额外优化:确保ViewModel更新触发视图刷新
因为League是struct,修改数组中元素的内部属性时,数组本身的引用不会变化,@Published可能无法触发刷新。修改loadSeasons方法,通过替换整个元素来确保通知发出:
func loadSeasons(forLeague id: String) async throws { let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api-football-standings.azharimm.site/leagues/\(id)/seasons")!) guard let seasons = try? JSONDecoder().decode(SeasonResponse.self, from: data) else { throw URLError(.cannotParseResponse) } await MainActor.run { if let responsedLeagueIndex = leagues.firstIndex(where: { $0.id == id }), let unwrappedSeasons = seasons.data?.seasons { // 创建新的League实例替换原数组中的元素 var updatedLeague = leagues[responsedLeagueIndex] updatedLeague.seasons = unwrappedSeasons leagues[responsedLeagueIndex] = updatedLeague print(unwrappedSeasons) } } }
原方案失效原因
@State var league是DetailView内部的状态,是ViewModel中league的值拷贝,ViewModel更新数组里的league时,这个拷贝不会自动同步。- 用
@Binding传递拷贝给ChangeButton,切换时只是把另一个league的拷贝赋值给它,依然和ViewModel的数据源脱节。
改用ID追踪后,DetailView始终从ViewModel的leagues数组中获取数据,ViewModel的数据更新会自动触发视图刷新,完美解决异步更新后的同步问题。
内容的提问来源于stack exchange,提问作者Nizami
相关产品推荐
相关产品推荐

