5

I have majority of the functionality working and returning exactly what I want. However, I'm having bit of a brain fart when it comes to taking the photos array in the response and assigning them to appropriate employees to be able to render them. Here's what's going on:

  1. There are 4x Codable structs: Response, Company, Employee, and ProfileImages. Response is the main object returned by the API and then decoded into Company having an array of [Employee], each have 3x ProfileImages (small, medium, and large in size)
  2. There's a companyPublisher that fetches the company details along with an array of employees
  3. Then there's a photosPublisher which takes employees array from the previous step and sequences them to be able to retrieve their profileImages.large profile image
  4. At last, I have a Publishers.Zip(companyPublisher, photosPublisher) that sets up the publisher's .sink() to respond with completion once everything requested has been fetched.

Can someone advise what would be the appropriate steps I need to take to be able to assign the correct employee image to the actual employee? I was thinking about setting up an optional UIImage type property inside the Employee codable struct but am still unsure on how I would go about assigning the appropriate Future object to that employee.

Any help would be greatly appreciated. Thanks in advance!

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 (Codable Structs):

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: $0.data) }
          .mapError { $0 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)
  }
}
Paul D.
  • 156
  • 2
  • 12
  • Does this help: https://stackoverflow.com/questions/61841254/combine-framework-how-to-process-each-element-of-array-asynchronously-before-pr/61968621#61968621 – New Dev Jul 14 '20 at 13:09
  • @NewDev yes that did help me originally in formulating my algorithm to output the correct response...which I have working! Now I’m just stuck on how to associate the appropriate nested future photo objects (being ProfileImage.large of each Employee) to their pertinent array element (being Employee in array of Employees returned with the Company) – Paul D. Jul 14 '20 at 13:18

1 Answers1

2

You're almost there, but you need to "zip" at an inner (nested) level, ie. inside the 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: $0.data) }
          .mapError { $0 as Error }

          .map { (employee, $0) } // "zip" here into a tuple

          .eraseToAnyPublisher()

   }
   .collect()

Now you have and array of tuples of employee and the profile image. Similarly, you could also retrieve all the profile images, for example, by using Publishers.Zip3.

EDIT

If you want to update the employee value instead of returning a tuple, you can instead return the updated employee:

// ...
   .map {
      var employeeCopy = employee
      employeeCopy.profileImageToShow = SomeImage(photo: $0)
      return employeeCopy
   }
// ...

What this gives you is an array of employees with profileImageToShow property set, which you can .zip with the original response, as you wanted:

Publishers.Zip(companyPublisher, employeesPublisher)
   .receive(on: DispatchQueue.main)
   .sink { (response, employees) in 
      self.company = response.company
      self.employees = employees
   }
   .store(in: &subscriptions)
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • Ok, using this method, now my output is the following -> `([success, company, [employee]], [[employee, UIImage]])`. It still seems redundant to have `[Employee]` array twice. When you say "_zip here into a tuple_" do you mean: `.map { Publishers.Zip(companyPublisher, (employee, $0)) }`? I guess I'm having a hard time understanding how to Zip inside a Publisher itself. All this async/future stuff is awesome but it definitely requires a paradigm shift in how our brain usually processes logic (which is generally sequential). – Paul D. Jul 14 '20 at 15:04
  • In my version, there would be no need for the final Zip that you had in your question. If you do `employeesPublisher.sink { ... }`, you'd get the array of employee-image tuples. – New Dev Jul 14 '20 at 15:08
  • Where are storing the profile `UIImage` objects? I just used a tuple as an example to show how you could correlate the employee with the retrieved profile image – New Dev Jul 14 '20 at 15:11
  • I updated my question with a new optional property inside `Employee` called `profileImageToShow` of type `SomeImage` which essentially is a wrapper for a `UIImage` that we are pulling from the API. – Paul D. Jul 14 '20 at 15:21
  • Ok... then you can just return the updated employee, instead of the tuple – New Dev Jul 14 '20 at 15:39
  • @PaulD. I updated the answer. Does this solve it for you? – New Dev Jul 14 '20 at 17:12
  • Sorry I was in meetings. I just tried your code and I'm stuck on the fact that it won't let me write back to Codable struct inside `.map()` stating `Cannot assign to property: employee is a let constant`. I tried to change all my structs to var with no difference. – Paul D. Jul 14 '20 at 19:16
  • @PaulD. right... that makes sense... create a copy instead. `var copyEmployee = employee` (updated the answer) – New Dev Jul 14 '20 at 19:18
  • BINGO!!! That's it...hard to show you my excitement on this side! Thank you sir!!! Marked as answer. – Paul D. Jul 14 '20 at 21:01