javierdemartin
javierdemartin

Reputation: 645

Swift Combine transform an array of publishers

I'm getting my way through Combine bit I don't seem to wrap my head around some trivial operations. I'm building a Combine based to do some HealthKit operations. Mainly I have two operations:

  1. func workouts(_ limit: Int) -> AnyPublisher<[HKWorkout], Error>
  2. func workoutDetails(_ workout: HKWorkout) -> AnyPublisher<WorkoutDetails, Error>

First off I will use the first one to query some workouts, that's easy.

HKHealthStore().workouts(HKObjectQueryNoLimit)
    .sink(receiveCompletion: { subject in
        switch subject {
        
        case .finished:
            break
        case .failure(let error):
            dump(error)
        }
    }, receiveValue: { workouts in
        dump(workouts)
        // What now? 
    }).store(in: &bag)  

Now I'd like to query the second function. It takes a workout as an argument and returns more information about the HKWorkout. How can I query the second function with the returned [HKWorkout] but don't know how to approach this.

What I have attempted so far it's not working.

HKHealthStore().workouts(HKObjectQueryNoLimit)
    .map({ workouts -> AnyPublisher<[WorkoutDetails], Never> in
        
        return workouts.publisher.flatMap({ workout in
            return Just(workout as! [WorkoutDetails]).eraseToAnyPublisher()
                .eraseToAnyPublisher()
        })
        .eraseToAnyPublisher()
        
    }).sink(receiveCompletion: { subs in
        switch subs {
        
        }
    }, receiveValue: { val in
        /// Cannot convert value of type 'AnyPublisher<[WorkoutDetails], Never>' to expected argument type 'Void'
        /// I am not able to get [WorkoutDetails] here as I'd like
        dump(val)
    })
    .store(in: &bag)

Edit: Solution I've finally used. There are two options that I've finally managed to make it work and understand it.

One is @New Dev's answer to finally receive in the sink the combined array and another one is to receive on the sink individual elements to save on an array.

HKHealthStore().workouts(HKObjectQueryNoLimit)
    .flatMap({
        $0.publisher.eraseToAnyPublisher()
    }).flatMap({
        $0.workoutWithDetails
    }).sink(receiveCompletion: { comp in
        switch comp {

        case .finished:
            break
        case .failure(let er):
            dump(er)
        }
    }, receiveValue: { details in
        /// Add the individual `WorkoutDetails` elements to an array 
        results.append(details)
    }).store(in: &SGHealthManager.bag)

Upvotes: 2

Views: 4764

Answers (1)

New Dev
New Dev

Reputation: 49590

The general approach is to flatMap the array into a Sequence publisher of individual values, then do any async operations on each value (another flatMap), then collect the results:

HKHealthStore()
   .workouts(HKObjectQueryNoLimit)
   .flatMap { workouts in workouts.publisher }
   .flatMap { workout in
       workoutDetails(workout)
   }
   .collect()
   .sink(
      receiveCompletion: { ... },
      receiveValue: { arrayOfWorkoutDetails in
          // arrayOfWorkoutDetails is of type [WorkoutDetails]
      })
   .store(in: &bag)

Upvotes: 2

Related Questions