You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI List无法同步显示ViewModel已加载的更新数据

问题根因
  • 多实例冲突:你在EventListView、eventCell、全局pullData()方法中分别创建了独立的EventsListViewModel实例,不同实例的状态完全不互通。下拉刷新时修改的是pullData()内新建的临时实例,和List绑定的ViewModel实例没有任何关联,自然无法触发视图更新。
  • 异步时序错误:loadCSV中的CSV下载是异步执行的,调用task.resume()后会立刻执行后续读本地文件、解析CSV的逻辑,此时新文件还未下载完成,读取到的永远是旧缓存;此外网络回调默认运行在后台线程,直接在后台线程修改@Published属性会导致UI不刷新甚至随机崩溃。
  • 冗余声明:eventCell仅需展示传入的event数据,不需要单独持有ViewModel实例,多余的@ObservedObject声明只会生成无用的新实例,没有实际作用。
  • 修饰符位置错误:.navigationTitle被放在NavigationView外部,无法正常生效。
修改方案
  1. 将所有数据加载逻辑收拢到ViewModel内部,删除全局的pullData、loadCSV、readCSV方法,保证所有状态修改都操作视图持有的同一个ViewModel实例。
  2. 修正异步下载逻辑:等文件下载完成、写入本地缓存后再解析新文件,解析完成后必须切换到主线程更新@Published属性。
  3. 删除所有多余的ViewModel实例创建,子视图仅接收传入的展示参数,不自行持有ViewModel。
  4. 调整导航栏修饰符到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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 06:36:17