Combine多请求链式调用:如何映射为[ExerciseDetails]数组
Combine嵌套请求构建ExerciseDetails数组解决方案
你的核心问题是现有Combine链在多次flatMap后丢失了原Exercise的上下文信息(如id、title),导致无法将图片、变体数据与原Exercise关联,最终无法组装成ExerciseDetails结构。需要在每一步请求中保留必要上下文,同时批量处理图片和变体请求。
假设的基础模型(需匹配API返回结构)
struct Exercise: Codable { let id: Int let title: String let images: [ExerciseImage] let variations: [Int] } struct ExerciseImage: Codable { let imageUrl: String } // 你定义的目标结构 struct ExerciseDetails: Hashable { let id: Int let title: String let image: [UIImage] let variations: [ExerciseDetails] }
修正后的Combine实现代码
import Combine import UIKit // 全局持有订阅,避免提前销毁 private var cancellables = Set<AnyCancellable>() // 辅助函数:根据ID获取完整Exercise数据 func fetchFullExercise(id: Int) -> AnyPublisher<Exercise, Error> { guard let url = URL(string: "https://wger.de/api/v2/exerciseinfo/\(id)") else { return Fail(error: URLError(.badURL)).eraseToAnyPublisher() } return URLSession.shared.dataTaskPublisher(for: url) .map(\.data) .decode(type: Exercise.self, decoder: JSONDecoder()) .eraseToAnyPublisher() } // 辅助函数:根据URL获取UIImage(处理空数据情况) func fetchImage(urlString: String) -> AnyPublisher<UIImage, Error> { guard let url = URL(string: urlString) else { return Fail(error: URLError(.badURL)).eraseToAnyPublisher() } return URLSession.shared.dataTaskPublisher(for: url) .map(\.data) .tryMap { data in guard let image = UIImage(data: data) else { throw NSError(domain: "ImageLoader", code: 0, userInfo: [NSLocalizedDescriptionKey: "无法从数据生成图片"]) } return image } .eraseToAnyPublisher() } // 递归函数:处理变体Exercise的嵌套请求 func fetchVariations(ids: [Int]) -> AnyPublisher<[ExerciseDetails], Error> { guard !ids.isEmpty else { return Just([]).setFailureType(to: Error.self).eraseToAnyPublisher() } return Publishers.Sequence(sequence: ids) .flatMap { id in fetchFullExercise(id: id) .flatMap { fullExercise in // 批量获取当前变体的所有图片 Publishers.Sequence(sequence: fullExercise.images) .flatMap { fetchImage(urlString: $0.imageUrl) } .collect() // 组合图片数组与变体请求结果 .zip(fetchVariations(ids: fullExercise.variations)) .map { images, variations in ExerciseDetails( id: fullExercise.id, title: fullExercise.title, image: images, variations: variations ) } } } .collect() .eraseToAnyPublisher() } // 主请求链:从初始列表到最终ExerciseDetails数组 func fetchAllExerciseDetails() { guard let initialUrl = URL(string: "https://wger.de/api/v2/exercise/?limit=70&offset=40") else { print("初始URL无效") return } URLSession.shared.dataTaskPublisher(for: initialUrl) .map(\.data) .decode(type: [Exercise].self, decoder: JSONDecoder()) .flatMap { exercises in Publishers.Sequence(sequence: exercises.map(\.id)) .flatMap { id in fetchFullExercise(id: id) .flatMap { fullExercise in // 批量获取当前Exercise的所有图片 Publishers.Sequence(sequence: fullExercise.images) .flatMap { fetchImage(urlString: $0.imageUrl) } .collect() // 组合图片数组与变体请求结果 .zip(fetchVariations(ids: fullExercise.variations)) .map { images, variations in ExerciseDetails( id: fullExercise.id, title: fullExercise.title, image: images, variations: variations ) } } } .collect() // 将所有ExerciseDetails收集为数组 } .receive(on: DispatchQueue.main) // 切换到主线程处理结果 .sink(receiveCompletion: { completion in switch completion { case .finished: break case .failure(let error): print("请求失败:\(error.localizedDescription)") } }, receiveValue: { detailsArray in // 这里拿到最终的[ExerciseDetails]数组 print("共获取到\(detailsArray.count)条完整训练详情") // 后续可用于UI展示 }) .store(in: &cancellables) } // 调用开始请求 fetchAllExerciseDetails()
关键实现说明
- 上下文保留:每一层
flatMap都持有当前fullExercise的引用,确保组装ExerciseDetails时能获取id、title等核心数据。 - 批量请求处理:用
Publishers.Sequence将数组转为Publisher,配合flatMap发起批量请求,最后用collect()将结果聚合成数组。 - 递归嵌套处理:变体本身也符合
ExerciseDetails结构,通过递归函数fetchVariations实现嵌套请求。 - 数据组合:用
zip操作符将图片数组和变体数组的请求结果合并,一次性映射为目标对象。 - 错误处理:每个步骤都处理了URL无效、图片生成失败等异常,确保错误能被上游捕获。
注意事项
- 可通过
flatMap(maxPublishers: .max(5))限制并发请求数,避免触发服务器限流。 - 需根据实际API返回结构调整
Exercise和ExerciseImage的Codable映射(比如添加CodingKeys处理字段名差异)。 - 在页面销毁时记得清空
cancellables,避免内存泄漏。
内容的提问来源于stack exchange,提问作者mike22
相关产品推荐
相关产品推荐

