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

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()

关键实现说明

  1. 上下文保留:每一层flatMap都持有当前fullExercise的引用,确保组装ExerciseDetails时能获取id、title等核心数据。
  2. 批量请求处理:用Publishers.Sequence将数组转为Publisher,配合flatMap发起批量请求,最后用collect()将结果聚合成数组。
  3. 递归嵌套处理:变体本身也符合ExerciseDetails结构,通过递归函数fetchVariations实现嵌套请求。
  4. 数据组合:用zip操作符将图片数组和变体数组的请求结果合并,一次性映射为目标对象。
  5. 错误处理:每个步骤都处理了URL无效、图片生成失败等异常,确保错误能被上游捕获。

注意事项

  • 可通过flatMap(maxPublishers: .max(5))限制并发请求数,避免触发服务器限流。
  • 需根据实际API返回结构调整Exercise和ExerciseImage的Codable映射(比如添加CodingKeys处理字段名差异)。
  • 在页面销毁时记得清空cancellables,避免内存泄漏。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 12:05:29