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

SwiftUI ForEach中ObservableObject加载所有项显示相同数据问题

问题根源

这个bug的核心原因非常明确:你把ChapterLoaderApi实例放在了父视图PostDetail层级,整个ForEach循环的所有条目都共用同一个loader实例、同一个state状态。
不管你点击哪个按钮,修改的都是这一个全局共享的state,SwiftUI刷新视图时,循环内所有条目都会读取同一个state值,自然全部渲染出一模一样的内容,根本无法区分哪个条目处于加载完成、哪个处于空闲状态。

修复方案

有两种常用实现方式,可按需选择:

方案1:拆分独立章节行视图,每行持有专属loader

把ForEach里的单条内容抽成独立子视图,给每个子视图单独创建@StateObject的loader实例,这样每个条目的状态完全隔离,不会互相干扰,也是SwiftUI推荐的封装写法。
修改后的完整代码如下:

// 抽离单条章节的独立行视图
struct ChapterRow: View {
    let item: ChapterMeta
    // 每个行视图持有独立的loader实例,状态不会和其他行共享
    @StateObject private var chapterLoader = ChapterLoaderApi()
    
    var body: some View {
        Button(action: {
            Task {
                await chapterLoader.fetchChapter(currentItem: item)
            }
        }, label: {
            Rectangle()
                .frame(width: 200, height: 100)
                .foregroundColor(.blue)
        })
        
        switch chapterLoader.state {
            case .idle: EmptyView()
            case .loading: ProgressView()
            case .loaded(let html):
                RichText(html: html.renderedContent)
                    .lineHeight(170)
                    .imageRadius(6)
                    .fontType(.system)
        
            case .failed(let error):
                Text(error.localizedDescription)
        }
    }
}

struct PostDetail: View {
    @State var postDetail: Post
    // 父视图不再需要持有全局共享的loader
    
    var body: some View {
      ScrollView(showsIndicators: false) {
        LazyVStack {
            ForEach(postDetail.chapters) { item in
                // 循环内直接渲染独立行视图
                ChapterRow(item: item)
            }
        }
      }
    }
}

// 原有ChapterLoaderApi逻辑无需修改
@MainActor class ChapterLoaderApi: ObservableObject {
    
    enum LoadingState {
        case idle
        case loading
        case loaded(Chapter)
        case failed(Error)
    }
    
    @Published var state: LoadingState = .idle
    
    func fetchChapter(currentItem item: ChapterMeta) async {
        
        self.state = .loading
        
        do {
            let response = try await getChapter(id: item.id)
            self.state = .loaded(response)
        } catch {
            state = .failed(error)
        }
    }
}

方案2:不拆子视图,用字典存储每个章节的独立状态

如果不想额外拆分子视图,可以直接在父视图维护一个[章节ID: 加载状态]的字典,点击按钮时只更新对应ID的状态,渲染时也只根据当前条目的ID取对应状态展示。
注意这个方案要求ChapterMeta的id属性全局唯一,否则会出现状态串扰。如果后续章节列表支持增删改,需要手动同步清理字典内的缓存状态,维护成本比方案1高。

两种方案对比:

  • 拆分子视图的方案逻辑更清晰,状态完全封装在单行内部,适合条目交互逻辑较复杂的场景
  • 字典存状态的方案无需新增视图类型,适合逻辑极简单的轻量场景
额外说明

LazyVStack本身会自动复用离屏视图,只要把状态绑定在对应条目自身的行视图上,复用逻辑不会导致状态错乱,无需额外处理。

内容的提问来源于stack exchange,提问作者alyx

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.01 03:39:43