Search code examples
iosswiftreactive-programmingcombinemoya

How to have multiple requests using Combine Framework


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>.


Solution

  • 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()