SwiftUI List无法同步显示ViewModel已加载的更新数据
问题根因
- 多实例冲突:你在
EventListView、eventCell、全局pullData()方法中分别创建了独立的EventsListViewModel实例,不同实例的状态完全不互通。下拉刷新时修改的是pullData()内新建的临时实例,和List绑定的ViewModel实例没有任何关联,自然无法触发视图更新。 - 异步时序错误:
loadCSV中的CSV下载是异步执行的,调用task.resume()后会立刻执行后续读本地文件、解析CSV的逻辑,此时新文件还未下载完成,读取到的永远是旧缓存;此外网络回调默认运行在后台线程,直接在后台线程修改@Published属性会导致UI不刷新甚至随机崩溃。 - 冗余声明:
eventCell仅需展示传入的event数据,不需要单独持有ViewModel实例,多余的@ObservedObject声明只会生成无用的新实例,没有实际作用。 - 修饰符位置错误:
.navigationTitle被放在NavigationView外部,无法正常生效。
修改方案
- 将所有数据加载逻辑收拢到ViewModel内部,删除全局的
pullData、loadCSV、readCSV方法,保证所有状态修改都操作视图持有的同一个ViewModel实例。 - 修正异步下载逻辑:等文件下载完成、写入本地缓存后再解析新文件,解析完成后必须切换到主线程更新
@Published属性。 - 删除所有多余的ViewModel实例创建,子视图仅接收传入的展示参数,不自行持有ViewModel。
- 调整导航栏修饰符到
NavigationView内部的正确位置。
修正后核心代码
EventsListViewModel
class EventsListViewModel: ObservableObject { @Published var events: [CSVEvent] = [] init() { // 初始化时先加载本地缓存,无网场景也能立刻展示内容避免空页 loadData(loadCacheFirst: true) } func loadData(loadCacheFirst: Bool = false) { let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let localFileUrl = documentsUrl.appendingPathComponent("testcsv.csv") guard let remoteUrl = URL(string: "https://example.com/testcsv.csv") else { return } // 首次加载先读缓存快速展示 if loadCacheFirst { self.events = parseCSV(at: localFileUrl).filter { !dateInPast(value: $0.date) } } // 拉取最新CSV let downloadTask = URLSession.shared.downloadTask(with: remoteUrl) { [weak self] tempUrl, response, error in guard let self = self, let tempUrl = tempUrl, error == nil else { print("CSV下载失败,将使用本地缓存数据") return } do { // 替换本地旧缓存文件 try? FileManager.default.removeItem(at: localFileUrl) try FileManager.default.copyItem(at: tempUrl, to: localFileUrl) // 必须切回主线程更新UI数据 DispatchQueue.main.async { self.events = self.parseCSV(at: localFileUrl).filter { !dateInPast(value: $0.date) } } } catch { print("本地文件写入失败: \(error)") } } downloadTask.resume() } // 抽离CSV解析逻辑,复用 private func parseCSV(at fileUrl: URL) -> [CSVEvent] { guard let content = try? String(contentsOf: fileUrl) else { return [] } var rows = content.components(separatedBy: "\n") guard !rows.isEmpty else { return [] } rows.removeFirst() // 移除表头行 return rows.compactMap { row in let columns = row.components(separatedBy: ",") return CSVEvent(raw: columns) } } }
EventListView
struct EventListView: View { // 用@StateObject持有ViewModel,保证生命周期和视图绑定(iOS14+支持,iOS13可替换为@ObservedObject并从上级传入实例) @StateObject private var filterviewModel = EventsListViewModel() var body: some View { NavigationView { List(filterviewModel.events, id: \.id) { event in EventCell(event: event) } .navigationTitle("Upcoming events") // 导航标题需放在NavigationView内部的子视图上 .refreshable { // 直接调用当前持有的ViewModel实例的加载方法 filterviewModel.loadData() } } } }
EventCell
// 按Swift命名规范,结构体名首字母大写 struct EventCell: View { let event: CSVEvent // 仅接收传入的展示数据,不需要持有ViewModel var body: some View { HStack{ VStack(alignment: .leading, spacing: 5){ Link(event.location, destination: URL(string: event.url)!) Text(event.eventname) .font(.subheadline) .foregroundColor(.secondary) }.frame(width: 200.0, alignment: .topLeading) VStack { HStack { Spacer() VStack (alignment: .trailing, spacing: 5){ Text(event.date) .fontWeight(.semibold) .lineLimit(2) .minimumScaleFactor(0.5) // 注意:原代码中actualtime未定义,需自行补全对应取值逻辑 Text(actualtime) .frame(alignment: .trailing) .font(.subheadline) .foregroundColor(.secondary) } } } } } }
补充说明
- 原代码中
loadCSV传入的csvName参数未实际使用,修正后已移除该无用参数。 - 解析CSV时使用
compactMap替代手动append,可自动跳过解析失败的无效行,避免崩溃。 - 若你的App需要适配iOS13,将
@StateObject替换为@ObservedObject即可,注意不要在子视图中重复初始化ViewModel。
内容的提问来源于stack exchange,提问作者Al McIver
相关产品推荐
相关产品推荐

