Swift 结合 URLSession 检索 Dataset/Photos 使用 2x Publishers

Swift Combine URLSession retrieving Dataset/Photos using 2x Publishers

我的大部分功能都能正常工作并返回我想要的内容。但是,当涉及到 response 中的 photos 数组并将它们分配给适当的 employees 以便能够渲染它们时,我有点脑残。这是发生了什么:

  1. 有 4 个 Codable 结构:ResponseCompanyEmployeeProfileImagesResponse是API返回的主对象,然后解码成Company有一个[Employee]的数组,每个有3xProfileImages(小,中,和大)
  2. 有一个 companyPublisher 可以获取 company 详细信息以及 employees
  3. 的数组
  4. 然后是一个 photosPublisher,它从上一步中获取 employees 数组并对它们进行排序,以便能够检索他们的 profileImages.large 个人资料图像
  5. 最后,我有一个 Publishers.Zip(companyPublisher, photosPublisher) 设置发布者的 .sink() 以在请求的所有内容都已获取后完成响应。

有人可以告诉我需要采取哪些适当的步骤才能将正确的员工形象分配给实际员工吗?我正在考虑在 Employee 可编码结构中设置一个可选的 UIImage 类型 property,但我仍然不确定我将如何为该员工分配适当的 Future 对象。

如有任何帮助,我们将不胜感激。提前致谢!

Response.JSON:

{
  "success": true,
  "company": {
    "id": 64,
    "name": "XYZ (Birmingham, AL)",
    "enabled": true
  },
  "employees": [{
    "id": 35,
    "name": "Chad Hughes",
    "email": "chad.hughes@company.com",
    "profileImages": {
      "small": "https://via.placeholder.com/150/09f/fff.png",
      "medium": "https://via.placeholder.com/300/09f/fff.png",
      "large": "https://via.placeholder.com/600/09f/fff.png"
    }
  }, {
    "id": 36,
    "name": "Melissa Martin",
    "email": "melissa.martin@company.com",
    "profileImages": {
      "small": "https://via.placeholder.com/150/F2A/fff.png",
      "medium": "https://via.placeholder.com/300/F2A/fff.png",
      "large": "https://via.placeholder.com/600/F2A/fff.png"
    }
  }]
}

Models.swift(可编码结构):

struct Response: Codable {
  let success: Bool
  let company: Company
  let employees: [Employee]
  let message: String?
}

struct Company: Codable, Identifiable {
  let id: Int
  let name: String
  let enabled: Bool
}

struct Employee: Codable, Identifiable {
  let id: Int
  let name: String
  let email: String
  let profileImages: ProfileImage
  let profileImageToShow: SomeImage?
}

struct SomeImage: Codable {
  let photo: Data
  init(photo: UIImage) {
    self.photo = photo.pngData()!
  }
}

struct ProfileImage: Codable {
  let small: String
  let medium: String
  let large: String
}

CompanyDetails.swift:

class CompanyDetails: ObservableObject {
  private let baseURL = "https://my.api.com/v1"
  
  @Published var company: Company = Company()
  @Published var employees: [Employee] = []
  
  var subscriptions: Set<AnyCancellable> = []
  
  func getCompanyDetails(company_id: Int) {
    let url = URL(string: "\(baseURL)/company/\(company_id)")
    
    // Company Publisher that retrieves the company details and its employees
    let companyPublisher = URLSession.shared.dataTaskPublisher(for url: url)
      .map(\.data)
      .decode(type: Response.self, decoder: JSONDecoder())
      .eraseToAnyPublisher()
    
    // Photo Publisher that retrieves the employee's profile image in large size
    let photosPublisher = companyPublisher
      .flatMap { response -> AnyPublisher<Employee, Error> in
        Publishers.Sequence(sequence: response.employees)
          .eraseToAnyPublisher()
      }
      .flatMap { employee -> AnyPublisher<UIImage, Error> in
        URLSession.shared.dataTaskPublisher(for url: URL(string: employee.profileImages.large)!)
          .compactMap { UIImage(data: [=14=].data) }
          .mapError { [=14=] as Error }
          .eraseToAnyPublisher()
      }
      .collect()
      .eraseToAnyPublisher()
    
    // Zip both Publishers so that all the retrieved data can be .sink()'d at once
    Publishers.Zip(companyPublisher, photosPublisher)
      .receive(on: DispatchQueue.main)
      .sink(
        receiveCompletion: { completion in
          print(completion)
        },
        receiveValue: { company, photos in
          print(company)
          print(photos)
        }
      )
      .store(in: &subscriptions)
  }
}

你快到了,但你需要在内部(嵌套)级别“压缩”,即。在 flatMap:

里面
let employeesPublisher = companyPublisher
   .flatMap { response in
      response.employees.publisher.setFailureType(Error.self)
   }
   .flatMap { employee -> AnyPublisher<(Employee, UIImage), Error> in

      let profileImageUrl = URL(string: employee.profileImages.large)!

      return URLSession.shared.dataTaskPublisher(for url: profileImageUrl)
          .compactMap { UIImage(data: [=10=].data) }
          .mapError { [=10=] as Error }

          .map { (employee, [=10=]) } // "zip" here into a tuple

          .eraseToAnyPublisher()

   }
   .collect()

现在你有了员工和个人资料图片的元组数组。同样,您也可以检索所有个人资料图片,例如,使用 Publishers.Zip3.

编辑

如果您想更新 employee 值而不是 return 元组,您可以 return 更新后的员工:

// ...
   .map {
      var employeeCopy = employee
      employeeCopy.profileImageToShow = SomeImage(photo: [=11=])
      return employeeCopy
   }
// ...

这为您提供了一组设置了 profileImageToShow 属性 的员工,您可以根据需要 .zip 使用原始响应:

Publishers.Zip(companyPublisher, employeesPublisher)
   .receive(on: DispatchQueue.main)
   .sink { (response, employees) in 
      self.company = response.company
      self.employees = employees
   }
   .store(in: &subscriptions)