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
相关产品推荐
相关产品推荐

