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

基于Async/Await复用网络请求获取多组书单数据问题求助

解决方案:在ViewModel中正确处理多组书单的异步请求

核心思路

把所有异步请求逻辑收拢到ListsViewModel中,通过批量异步处理+状态管理解决顺序和效率问题,避免视图层直接处理网络请求。

步骤1:完善BookService的批量请求方法

给BookService添加批量获取书籍的异步方法,自带错误处理,同时支持单本和多本请求:

class BookService {
    // 单本书籍请求
    func fetchBook(by id: String) async throws -> Book {
        let url = URL(string: "https://www.googleapis.com/books/v1/volumes/\(id)")!
        let (data, response) = try await URLSession.shared.data(from: url)
        guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            throw BookServiceError.invalidResponse
        }
        return try JSONDecoder().decode(Book.self, from: data)
    }
    
    // 批量书籍请求,返回[ID: Book结果]字典,方便分组匹配
    func fetchBooks(by ids: [String]) async -> [String: Result<Book, Error>] {
        let tasks = ids.map { id in
            Task { () -> (String, Result<Book, Error>) in
                do {
                    let book = try await fetchBook(by: id)
                    return (id, .success(book))
                } catch {
                    return (id, .failure(error))
                }
            }
        }
        
        let results = await tasks.map { try! await $0.value }
        return Dictionary(uniqueKeysWithValues: results)
    }
    
    enum BookServiceError: Error {
        case invalidResponse
        case decodingFailed
    }
}

步骤2:在ListsViewModel中管理状态与异步逻辑

ViewModel维护四组书单的加载状态、数据、错误信息,用Task封装异步操作,避免重复请求:

class ListsViewModel: ObservableObject {
    @Published var currentList: [Book] = []
    @Published var nextList: [Book] = []
    @Published var doneList: [Book] = []
    @Published var sharingList: [Book] = []
    
    @Published var isLoading = false
    @Published var errorMessage: String?
    
    private let bookService: BookService
    private var loadTask: Task<Void, Never>?
    
    init(bookService: BookService = BookService()) {
        self.bookService = bookService
    }
    
    // 加载所有书单的入口方法
    func loadAllLists() {
        // 取消之前的任务,避免重复请求
        loadTask?.cancel()
        
        isLoading = true
        errorMessage = nil
        
        loadTask = Task { [weak self] in
            guard let self = self, !Task.isCancelled else { return }
            
            do {
                // 1. 从Firestore获取四组书单的ID数组
                let currentIds = try await self.fetchIdsFromFirestore(for: "current")
                let nextIds = try await self.fetchIdsFromFirestore(for: "next")
                let doneIds = try await self.fetchIdsFromFirestore(for: "done")
                let sharingIds = try await self.fetchIdsFromFirestore(for: "sharing")
                
                // 2. 合并所有ID批量请求,减少API调用次数
                let allIds = currentIds + nextIds + doneIds + sharingIds
                let bookResults = await self.bookService.fetchBooks(by: allIds)
                
                // 3. 按分组匹配书籍,主线程更新UI
                await MainActor.run {
                    self.currentList = currentIds.compactMap {
                        guard case .success(let book) = bookResults[$0] else { return nil }
                        return book
                    }
                    self.nextList = nextIds.compactMap {
                        guard case .success(let book) = bookResults[$0] else { return nil }
                        return book
                    }
                    self.doneList = doneIds.compactMap {
                        guard case .success(let book) = bookResults[$0] else { return nil }
                        return book
                    }
                    self.sharingList = sharingIds.compactMap {
                        guard case .success(let book) = bookResults[$0] else { return nil }
                        return book
                    }
                }
            } catch {
                await MainActor.run {
                    self.errorMessage = "加载书单失败:\(error.localizedDescription)"
                }
            }
            
            await MainActor.run {
                self.isLoading = false
            }
        }
    }
    
    // 从Firestore获取对应分组的书籍ID
    private func fetchIdsFromFirestore(for listType: String) async throws -> [String] {
        let snapshot = try await Firestore.firestore().collection("userLists").document(listType).getDocument()
        return snapshot.data()?["bookIds"] as? [String] ?? []
    }
    
    // 页面消失时取消任务,避免无效请求
    func cancelLoadTask() {
        loadTask?.cancel()
    }
}

步骤3:视图层正确调用ViewModel

在个人页面视图中,通过onAppear触发加载,onDisappear取消任务:

struct ProfileView: View {
    @StateObject private var viewModel = ListsViewModel()
    
    var body: some View {
        ScrollView {
            if viewModel.isLoading {
                ProgressView("加载中...")
            } else if let error = viewModel.errorMessage {
                Text(error)
                    .foregroundColor(.red)
            } else {
                BookListView(title: "当前阅读", books: viewModel.currentList)
                BookListView(title: "待读", books: viewModel.nextList)
                BookListView(title: "已读完", books: viewModel.doneList)
                BookListView(title: "分享中", books: viewModel.sharingList)
            }
        }
        .onAppear {
            viewModel.loadAllLists()
        }
        .onDisappear {
            viewModel.cancelLoadTask()
        }
    }
}

struct BookListView: View {
    let title: String
    let books: [Book]
    
    var body: some View {
        Section {
            Text(title)
                .font(.headline)
            ForEach(books) { book in
                BookRow(book: book)
            }
        }
        .padding()
    }
}

关键优化点

  • 批量请求合并:合并四组ID一次性请求,减少API调用次数,规避Google Books API限流
  • 状态集中管理:所有加载状态、错误信息由ViewModel维护,视图仅负责渲染
  • 任务取消机制:页面消失时取消未完成请求,避免内存泄漏和无效请求
  • 主线程更新UI:用MainActor.run确保UI操作在主线程执行,避免线程冲突

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 09:57:48