Search code examples
swiftapirequestcombinepublisher

Swift Combine complex api request


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.


Solution

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