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

SwiftUI进入详情视图后如何发起额外API请求并避免观察者报错?

问题解决方案

报错根本原因

你遇到的UIScrollView does not support multiple observers implementing报错核心原因:
你把书籍列表、书籍详情两个页面的状态都耦合在同一个APIManager实例中,当详情页拉取章节更新@Published属性时,会同时触发所有订阅了该APIManager的视图重绘。导航栈中已经压入后台的书籍列表页的List(底层是UIScrollView)和当前详情页的视图同时监听同一个可观察对象的状态变更,就会触发这个系统级冲突。
另外你原有写法还存在隐患:在View内部初始化@ObservedObject,当视图触发重绘时,APIManager实例会被反复重建,容易导致状态丢失、重复请求等问题。

正确实现流程

第一步:拆分业务状态,避免耦合

推荐按页面拆分可观察对象,实现状态隔离:

// 书籍列表页单独的ViewModel
class BookListViewModel: ObservableObject {
    @Published var books = [Book]()
    
    func fetchBooks() {
        // 原有拉取书籍列表逻辑保持不变
        if let url = URL(string: urlEndpoint) {
            let session = URLSession(configuration: .default)
            let task = session.dataTask(with: url) { (data, response, error) in
                if error == nil, let safeData = data {
                    do {
                        let response = try JSONDecoder().decode([Book].self, from: safeData)
                        DispatchQueue.main.async {
                            self.books = response
                        }
                    } catch {
                        print(error)
                    }
                }
            }
            task.resume()
        }
    }
}

// 书籍详情页单独的ViewModel
class BookDetailViewModel: ObservableObject {
    @Published var chapters = [Chapter]()
    
    func fetchChapterDetails(bookId: Int) {
        // 新增拉取章节逻辑,传入书籍id作为参数
        guard let url = URL(string: "\(chapterEndpoint)/\(bookId)") else { return }
        let session = URLSession(configuration: .default)
        let task = session.dataTask(with: url) { [weak self] data, _, error in
            guard let self = self, error == nil, let safeData = data else { return }
            do {
                let chapters = try JSONDecoder().decode([Chapter].self, from: safeData)
                DispatchQueue.main.async {
                    self.chapters = chapters
                }
            } catch {
                print(error)
            }
        }
        task.resume()
    }
}

第二步:优化BookView的对象持有方式

iOS14及以上版本,View内部初始化的可观察对象要用@StateObject代替@ObservedObject,保证视图重绘时实例不丢失:

struct BookView: View {
    @StateObject var viewModel = BookListViewModel()
    
    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.books) { book in
                    NavigationLink(
                        destination: BookDetailView(bookId: book.id),
                        label: {
                            HStack {
                                Text(book.name)
                                Spacer()
                                Text(book.testament)
                            }
                        })
                    
                }.navigationBarTitle("书目")
            }.onAppear {
                self.viewModel.fetchBooks()
            }
        }
    }
}

第三步:实现BookDetailView逻辑

struct BookDetailView: View {
    let bookId: Int
    @StateObject private var viewModel = BookDetailViewModel()
    
    var body: some View {
        List(viewModel.chapters) { chapter in
            // 渲染章节内容
            Text("第\(chapter.number)章:\(chapter.name)")
        }
        .navigationTitle("章节列表")
        .onAppear {
            viewModel.fetchChapterDetails(bookId: bookId)
        }
    }
}

兼容说明

如果需要兼容iOS13以下版本,可将ViewModel在父视图初始化后通过@ObservedObject传入子视图,不要在子视图内部初始化@ObservedObject即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.04 16:12:01