0

I'm currently using Moya alpha 15 with Combine Framework for my SwiftUI project. With Moya, I have a provider that's responsible for creating requests.

What I want:

  1. Use getInstance(page: Int) to get my initial instanceResponseList object.
  2. From that instanceResponseList object, check each instance if hasChildren == true
  3. If hasChildren == true, call getInstanceChildren(id: String) using the instance's id
  4. response from getInstanceChildren(id: String) will be mapped and assigned to the children: [Instance] property(response.data.instances)

Is this possible? If not, is there a better way to do this?

What I'm trying to do:

I need to show a profile image using the profileURL from Instance in a tableView. The height of each cell will be dynamic and based on the aspect ratio of each image. Each cell could have 1 + children profile images arranged differently.

Some sample code of my service call and data models:

    public struct InstanceResponseList: Codable {
        public var success: Bool
        public var data: InstanceResponse
    }

    public struct InstanceResponse: Codable {
        public var instances: [Instance]
        public var hasMore: Bool //for pagination
    }

    public struct Instance: Codable {
        public var id: String
        public var profileURL: String?
        public var hasChildren: Bool


        public var children: [Instance] // I want to make a request and append the children for each of my instances.

        enum CodingKeys: String, CodingKey {
            case id = "instance_id"
            case profileURL = "profile_url"
            case hasChildren = "has_children"
        }
    }

    public func getInstance(page: Int) -> AnyPublisher<InstanceResponseList, MoyaError> {
        return instanceProvider
            .requestPublisher(.allInstances(page: page, show: 10)) // page & show are parameters used for pagination, not relevant here
            .map(InstanceResponseList.self)
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }

    public func getInstanceChildren(id: String) -> AnyPublisher<InstanceResponseList, MoyaError> {
        return haptagramProvider
            .requestPublisher(.children(id: id))
            .map(InstanceResponseList.self)
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }

My attempt:

    public func getInstanceWithChildren(page: Int) -> AnyPublisher<[Instance], MoyaError> {
        
        return getInstance(page: Int)
            .flatMap { instanceResponseList -> AnyPublisher<Instance, MoyaError> in
                Publishers.Sequence(sequence: instanceResponseList.data.instances).eraseToAnyPublisher()
            }
            .flatMap { instance -> AnyPublisher<Instance, MoyaError> in
                return getInstanceChildren(id: instance.id).map {
                    let instance = instance
                    instance.children = $0
                    return instance
                }
                .eraseToAnyPublisher()
                
            }
            .collect()
            .eraseToAnyPublisher()
    }

which returns AnyPublisher<[Instance], MoyaError>, but I'm looking to return AnyPublisher<InstanceResponseList, MoyaError>.

iamarnold
  • 705
  • 1
  • 8
  • 22
  • Does this answer your question? [Combine framework: how to process each element of array asynchronously before proceeding](https://stackoverflow.com/questions/61841254/combine-framework-how-to-process-each-element-of-array-asynchronously-before-pr)... In short, use a `flatMap` after `getInstance` to return `getInstanceChildren` – New Dev Nov 15 '20 at 20:44
  • @NewDev I think this is similar to what I'm looking for, but the difference is that I need to go down a level to access my array of instances(InstanceResponseList.data.instances) from my `getInstance`. How do I handle that? – iamarnold Nov 15 '20 at 22:22
  • The "go down a level" is exactly what the question/answer I linked to solves – New Dev Nov 15 '20 at 22:54
  • @NewDev: I updated my question with my attempt. I'm still new to Combine and not very familiar. Would appreciate it if you could take a look and see if I'm doing it right. Thanks! – iamarnold Nov 15 '20 at 23:48

1 Answers1

2

What you did is mostly correct. You just need another level of nesting to be able to get the original instanceResponseList value:

getInstance(page: page)
   .flatMap { instanceResponseList in
       Publishers.Sequence(sequence: instanceResponseList.data.instances)
          .flatMap { instance in

              instance.hasChildren

                 ? getInstanceChildren(id: instance.id)
                      .map { children -> Instance in
                         var instance = instance
                         instance.children = children
                         return instance
                      }
                      .eraseToAnyPublisher()

                 : Just(instance)
                      .setFailureType(to: MoyaError.self)
                      .eraseToAnyPublisher()

          }
          .collect()
          .map { instances -> InstanceResponseList in
              var instanceResponseList = instanceResponseList
              instanceResponseList.data.instances = instances
              return instanceResponseList
          }
   }
   .eraseToAnyPublisher()
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • Amazing, it works! Another improvement I'm trying make is how do I call `getInstanceChildren` for each instance where `instance.hasChildren == true`. I tried `Publishers.Sequence(sequence: instanceResponseList.data.instances.filter({$0.hasChildren)`, but the instanceResponseList only gives me instances that has children. Again, thank you very much for you help. – iamarnold Nov 16 '20 at 01:53
  • thank you! I'm assuming I just have to continue nesting for each instance if I need to make a request to get the profileURL and assign it back to the instance(I'll need to add a UIImage property to Instance)? – iamarnold Nov 16 '20 at 02:42
  • You nest when a subsequent results depends on the current. But if you need to make multiple independent requests, you can make them in parallel, and maybe `Publishers.Zip` them into a tuple of results – New Dev Nov 16 '20 at 02:47