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.