Home > other >  Using Combines zip with two api calls that return Result<T, Error>
Using Combines zip with two api calls that return Result<T, Error>

Time:09-23

I have two api calls using Combine and Result that I need to merge so that i can create a new objects from the two.

  public func getPosts() -> AnyPublisher<Result<[Post], Error>, Never> {
    let client = ForumClient()
    return client.execute(.posts)
  }

  public func getUsers() -> AnyPublisher<Result<[User], Error>, Never> {
    let client = ForumClient()
    return client.execute(.users)
  }

I have working code, but I feel like I'm missing some syntactic sugar that can avoid the two switches.

    Publishers.Zip(getPosts(), getUsers())
      .sink(
        receiveValue: { [weak self] postsResult, usersReult in
          guard let self = self else {
            return
          }

          // Get Posts
          var posts: [Post] = []
          switch postsResult {
          case .failure(let error):
            print("Error: \(error.localizedDescription)")
          case .success(let postsArray):
            posts = postsArray
          }

          // Get Users
          var users: [User] = []
          switch usersReult {
          case .failure(let error):
            print("Error: \(error.localizedDescription)")
          case .success(let usersArray):
            users = usersArray
          }


          //Combine posts and users
          var forumPosts: [ForumPost] = []

          posts.forEach { post in
            users.forEach { user in
              if user.id == post.userId {
                let forumPost = ForumPost(username: user.username, title: post.title)
                forumPosts.append(forumPost)
              }
            }
          }

          print(forumPosts)

        })
      .store(in: &publishers)

Is there a better way to do this that avoids using two switches etc?

CodePudding user response:

Answering whether another way is "better" is hard.

At some point, you're going to have to deal with the fact that one of your requests for posts or users might fail. That's going to require looking at a Result. So far as I know, that can be done in one of two ways. Either using a switch or using the get() throws function in Result. The advantage of the switch is that you can look at the error and do something.

What I might choose to do, is put the error handling a bit closer to where the error might occur. I would want to have getPosts return a publisher of posts. If an error occurs I might emit the error inside that but still return all the posts I can get. Similarly for users.

Consider the following code that I put together in a Playground:

import UIKit
import Combine

struct Post {
    let userId: Int
    let title : String
}

struct User {
    let userId: Int
    let username : String
}

struct ForumPost {
    let username: String
    let title: String
}

func clientGetPosts() -> AnyPublisher<Result<[Post], Error>, Never> {

    let results =
        [ Result<[Post], Error>.success([
            Post( userId: 1, title: "The World According to Alice"),
            Post( userId: 2, title: "The World Accoording to Bob")
        ]) ]
    return results.publisher.eraseToAnyPublisher()
}

func clientGetUsers() -> AnyPublisher<Result<[User], Error>, Never> {

    let results =
        [ Result<[User], Error>.success([
            User(userId: 1, username: "Alice"),
            User(userId: 2, username: "Bob")
        ]) ]
    return results.publisher.eraseToAnyPublisher()
}

func getPosts() -> AnyPublisher<[Post], Never> {
    clientGetPosts().reduce([]) {
        (allPosts : [Post], postResult: Result<[Post], Error>) -> [Post]  in

        var newCollection = allPosts
        switch postResult {
            case .success(let newPosts) :
                newCollection.append(contentsOf: newPosts)

            case .failure(let error) :
                print("Retrieving posts errored: \(error.localizedDescription)")
        }

        return newCollection
    }.eraseToAnyPublisher()
}

func getUsers() -> AnyPublisher<[User], Never> {
    clientGetUsers().reduce([]) {
        (allUsers : [User], usersResult: Result<[User], Error>) -> [User]  in

        var newCollection = allUsers
        switch usersResult {
            case .success(let newUsers) :
                newCollection.append(contentsOf: newUsers)

            case .failure(let error) :
                print("Retrieving users errored: \(error.localizedDescription)")
        }

        return newCollection
    }.eraseToAnyPublisher()
}

var users : [User] = []
getUsers().sink { users = $0 }

let forumPosts = getPosts().flatMap {
    (posts: [Post]) -> AnyPublisher<ForumPost, Error> in
    return posts.publisher.tryMap {
        (post:Post) throws -> ForumPost in

        if let user = users.first(where: { $0.userId == post.userId }) {
            return ForumPost(username: user.username, title: post.title)
        } else {
            print("Post with title \"\(post.title)\" has an unknown userId")
            throw NSError(domain: "posters", code: -1, userInfo: nil)
        }
    }.eraseToAnyPublisher()
}.eraseToAnyPublisher()

forumPosts.sink(receiveCompletion: { _ in () },
                receiveValue: {
                    (forumPost: ForumPost) in
                    debugPrint(forumPost)

                })

the function clientGetPosts stands in for client.execute(.posts). I've changes getPosts so that it collects all the posts and emits errors, but returns all the posts it can find. There's still a switch statement, but it's closer to where the error is discovered.

Users are handled similarly.

At the bottom, I just grab the list of users. Because of the way we've set up getUsers it will grab as many users as it can and print out errors if any of the clients requests fail.

It then grabs the sequence that returns all the posts and uses flatMap to convert that into a sequence of ForumPosts with an error emitted for any forum post where a corresponding user can't be found.

CodePudding user response:

You can combine the results into a switch statement:

switch (usersResult, postsResult) {
case (.success(let users), .success(let posts)):
    print(makeForumPostsFrom(users: users, posts: posts))

case (.failure(let userError), .failure(let postError)):
    print("UserError: \(userError)")
    print("PostError: \(postError)")

case (.failure(let userError), _):
    print("UserError: \(userError)")

case (_, .failure(let postError)):
    print("PostError: \(postError)")
}

I created a utility method to create a ForumPost array from the resulting User and Post arrays:

func makeForumPostsFrom(users: [User], posts: [Post]) -> [ForumPost] {
   users.reduce(into: .init()) { forumPosts, user in
        let usersForumPosts = posts
            .filter { post in
                post.userId == user.id
            }
            .map { post in
                ForumPost(username: user.username, title: post.title)
            }
        forumPosts.append(contentsOf: usersForumPosts)
    }
}

All I did here was create an empty array of ForumPost, then for each user filter for their posts, then map those posts to a corresponding ForumPost, then store the results in the initial array.

  • Related