【问题标题】:SwiftUI: @State value doesn't update after async network requestSwiftUI:异步网络请求后@State 值不更新
【发布时间】:2022-08-14 17:22:03
【问题描述】:

我的目标是DetailView() 中的更改数据。通常在这种情况下,我使用@State + @Binding 并且它可以很好地处理静态数据,但是当我尝试使用来自网络请求的数据更新 ViewModel 时,我失去了@State 的功能(新数据不会传递给@State 值,它保持为空)。我检查了网络请求和解码过程 - 一切正常。抱歉,我的代码示例有点长,但这是我发现重现问题的最短方法...

楷模:

    struct LeagueResponse: Decodable {
        var status: Bool?
        var data: [League] = []
    }
    
    struct League: Codable, Identifiable {
        let id: String
        let name: String
        var seasons: [Season]?
        
    }
    
    struct SeasonResponse: Codable {
        var status: Bool?
        var data: LeagueData?
    }
    
    struct LeagueData: Codable {
        let name: String?
        let desc: String
        let abbreviation: String?
        let seasons: [Season]
    }
    
    struct Season: Codable {
        let year: Int
        let displayName: String
        
    }

视图模型:

class LeagueViewModel: ObservableObject {
    @Published var leagues: [League] = []
    
    init() {
        Task {
            try await getLeagueData()
        }
    }
    
    private func getLeagueData() async throws {
        let (data, _) = try await URLSession.shared.data(from: URL(string: \"https://api-football-standings.azharimm.site/leagues\")!)
        guard let leagues = try? JSONDecoder().decode(LeagueResponse.self, from: data) else {
            throw URLError(.cannotParseResponse)
        }
        await MainActor.run {
            self.leagues = leagues.data
        }
    }
    
    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 {
                leagues[responsedLeagueIndex].seasons = unwrappedSeasons
                print(unwrappedSeasons) // successfully getting and parsing data
            }
        }
    }
}

意见:

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(league: league)) {
                                Text(league.name)
                            }
                        }
                    }
                }
            }
            .navigationBarTitle(Text(\"Leagues\"), displayMode: .large)
        }
        .environmentObject(vm)
    }
}

struct DetailView: View {

    @EnvironmentObject var vm: LeagueViewModel
    @State var league: League
    
    var body: some View {
        VStack {
            if let unwrappedSeasons = league.seasons {
                List {
                    ForEach(unwrappedSeasons, id: \\.year) { season in
                        Text(season.displayName)
                    }
                }
            } else {
                ProgressView()
            }
        }
        .onAppear {
            Task {
                try await vm.loadSeasons(forLeague: league.id)
            }
        }
        .navigationBarTitle(Text(\"League Detail\"), displayMode: .inline)
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                ChangeButton(selectedLeague: $league)
            }
        }
    }
}

struct ChangeButton: View {
    
    @EnvironmentObject var vm: LeagueViewModel
    @Binding var selectedLeague: League // if remove @State the data will pass fine
    
    var body: some View {
        Menu {
            ForEach(vm.leagues) { league in
                Button {
                    self.selectedLeague = league
                } label: {
                    Text(league.name)
                }
            }
        } label: {
            Image(systemName: \"calendar\")
        }
    }
}

主要目标:

  1. DetailView() 中显示选定的联赛赛季数据
  2. 当在ChangeButton() 中选择另一个联赛时,可以更改DetailView() 中的赛季数据
  • 我会在详细视图中使 vm 成为 @ObservedObject 并从父视图中传递它
  • @JoakimDanielson 将 @EnvironmentObject 更改为 @ObservedObject 并将 @StateObject 传递给视图 - 结果相同,没有任何变化

标签: swift swiftui async-await binding state


【解决方案1】:

您更新视图模型,但 DetailView 包含复印件联盟的(因为它是值类型)

在我看来最简单的就是在回调赛季回归,所以也有可能更新本地联赛

func loadSeasons(forLeague id: String, completion: (([Season]) -> Void)?) async throws {

     // ...

    await MainActor.run {
        if let responsedLeagueIndex = leagues.firstIndex(where: { $0.id == id }),
           let unwrappedSeasons = seasons.data?.seasons {
            leagues[responsedLeagueIndex].seasons = unwrappedSeasons

            completion?(unwrappedSeasons) // << here !!
        }
    }
}

并使任务依赖于联赛 ID,因此选择会起作用,例如

struct DetailView: View {

    @EnvironmentObject var vm: LeagueViewModel
    @State var league: League

    var body: some View {
        VStack {
            // ... 
        }
        .task(id: league.id) {    // << here !!
            Task {
                try await vm.loadSeasons(forLeague: league.id) {
                    league.seasons = $0  // << update local copy !!
                }
            }
        }

使用 Xcode 13.4 / iOS 15.5 测试

Test module is here

【讨论】:

  • 稍有改进将不仅捕获[Season],而且捕获整个League。目前league.name 没有改变,尽管其他数据会改变。
【解决方案2】:

一个问题是,如果您已经创建了LeagueViewModelObservableObject,为什么不直接从中显示,而只需将id 传递给您的DetailView

因此,您的详细视图将是:

struct DetailView: View {
    @EnvironmentObject var vm: LeagueViewModel
    @State var id: String
    
    var body: some View {
        VStack {
            if let unwrappedSeasons = vm.leagues.first { $0.id == id }?.seasons {
                List {
                    ForEach(unwrappedSeasons, id: \.year) { season in
                        Text(season.displayName)
                    }
                }
            } else {
                ProgressView()
            }
        }
        .task(id: id) {
            Task {
                try await vm.loadSeasons(forLeague: id)
            }
        }
        .navigationBarTitle(Text("League Detail"), displayMode: .inline)
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                ChangeButton(selectedId: $id)
            }
        }
    }
}

该视图将在加载季节数据时自动更新它们。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-29
    • 1970-01-01
    • 1970-01-01
    • 2021-06-16
    • 1970-01-01
    相关资源
    最近更新 更多