Swift Combine URLSession для получения набора данных / фотографий с помощью 2x Publishers - PullRequest
2 голосов
/ 14 июля 2020

У меня большая часть функций работает и возвращает именно то, что я хочу. Тем не менее, у меня есть немного мозгов, когда дело доходит до того, чтобы взять массив photos в response и назначить их подходящим employees, чтобы иметь возможность их отображать. Вот что происходит:

  1. Есть 4x кодируемых структуры: Response, Company, Employee и ProfileImages. Response - это основной объект, возвращаемый API, а затем декодированный в Company, имеющий массив [Employee], каждый из которых имеет 3x ProfileImages (маленький, средний и большой размер)
  2. Там companyPublisher, который извлекает данные company вместе с массивом employees
  3. Затем есть photosPublisher, который берет employees массив из предыдущего шага и упорядочивает их, чтобы иметь возможность получить их profileImages.large изображение профиля
  4. Наконец, у меня есть Publishers.Zip(companyPublisher, photosPublisher), который настраивает .sink() издателя для ответа с завершением, как только все запрошенное будет получено.

Может кто-нибудь посоветует Какие шаги мне нужно предпринять, чтобы иметь возможность назначить правильный образ сотрудника фактическому сотруднику? Я думал о настройке необязательного UIImage типа property внутри кодируемой структуры Employee, но все еще не уверен, как я могу go назначить соответствующий объект 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: $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)
  }
}

1 Ответ

1 голос
/ 14 июля 2020

Вы почти у цели, но вам нужно «застегнуть» на внутреннем (вложенном) уровне, ie. внутри 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()

Теперь у вас есть массив кортежей сотрудника и изображение профиля. Точно так же вы также можете получить все изображения профиля, например, используя Publishers.Zip3.

EDIT

Если вы хотите вместо этого обновить значение employee Для возврата кортежа вы можете вместо этого вернуть обновленного сотрудника:

// ...
   .map {
      var employeeCopy = employee
      employeeCopy.profileImageToShow = SomeImage(photo: $0)
      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)
...