1

I just started to learn Combine and therefore I can't figure out how to make a complex request to the API.

It is necessary to create an application where the user can enter the name of the company's GitHub account in the input field and get a list of open repositories and their branches.

There are two API methods:

  1. https://api.github.com/orgs/<ORG_NAME>/repos This method returns a list of organization account repositories by name. For example, you can try to request a list of Apple's repositories https://api.github.com/orgs/apple/repos

struct for this method

struct Repository: Decodable {

 let name: String

 let language: String?

    enum Seeds {
        public static let empty = Repository(name: "", language: "")
    }

}
  1. https://api.github.com/repos/<ORG_NAME>/<REPO_NAME>/branches This method will be needed to get the branch names in the specified repository.

struct for this method

struct Branch: Decodable {

 let name: String

}

As a result, I need to get an array of such structures.

struct BranchSectionModel {
    var name: Repository
    var branchs: [Branch]
}

For this I have two functions:

func loadRepositorys(orgName: String) -> AnyPublisher<[Repository], Never> {
        
        guard let url = URL(string: "https://api.github.com/orgs/\(orgName)/repos" ) else {
            return Just([])
                .eraseToAnyPublisher()
        }
        
        return URLSession.shared.dataTaskPublisher(for: url)
            .map { $0.data }
            .decode(type: [Repository].self, decoder: JSONDecoder())
            .replaceError(with: [])
            .receive(on: RunLoop.main)
            .eraseToAnyPublisher()
    }

and

func loadBranchs(orgName: String, repoName: String) -> AnyPublisher<[Branch], Never> {
        guard let url = URL(string: "https://api.github.com/repos/\(orgName)/\(repoName)/branches") else {
            return Just([])
                .eraseToAnyPublisher()
        }
        
        return URLSession.shared.dataTaskPublisher(for: url)
            .map { $0.data }
            .decode(type: [Branch].self, decoder: JSONDecoder())
            .replaceError(with: [])
            .receive(on: RunLoop.main)
            .eraseToAnyPublisher()
        
    }

Both of these functions work separately, but I don't know how to end up with an [BranchSectionModel] . I guess to use flatMap and sink, but don't understant how.

I do not understand how to combine these two requests in one thread.

1 Answers1

1

When you're looking to convert one publisher into another, .map and .switchToLatest. In this case, since you're also looking to turn one publisher into many (and then back down into one), MergeMany will also be a useful tool:

loadRepositorys(orgName: orgName)
    .map { repos in
        Publishers.MergeMany(repos.map { repo in
            loadBranchs(orgName: orgName, repoName: repo.name)
                .map { branches in
                    BranchSectionModel(name: repo, branchs: branches)
                }
        })
        .collect(repos.count)
    }
    .switchToLatest()
    .sink { result in
        print("---")
        print(result)
    }
    .store(in: &cancellables)

Although I'm a big fan of Combine, I don't think it's particularly well suited to this task, compared with async/await, which will probably be a little less confusing and look cleaner. As a learning exercise, it's a great one, but if you were to tackle this problem in the real world, async/await would likely be my go-to.

jnpdx
  • 45,847
  • 6
  • 64
  • 94
  • Thanks for the advice. Your method works, but the problem is that it returns single elements. And I would like to get an array of such elements immediately at the output. P.S. Maybe you know where you can see examples with similar chains of requests? Unfortunately, I haven't been able to find it yet. – Evgenii Burdiuzha Oct 26 '22 at 10:36
  • I've added `.collect(repos.count)` to the end of the `MergeMany`, which now changes the output to a single array. Hopefully this fits what you're looking for. – jnpdx Oct 26 '22 at 14:04