Search code examples
iosswiftcombinepublisher

Combine conversion from model to viewModel gives error "Type of expression is ambiguous without more context when running .eraseToAnyPublisher()


I am trying to figure out why the last .eraseToAnyPublisher() is giving the aforementioned error, to me it seems all the types are well specified, aren't they?

static func searchUsers(query: String) -> AnyPublisher<[UserViewModel], Never> {
            // prepare URL
            let urlString = "\(baseURL)/search/users?q=\(query)"
            guard let url = URL(string: urlString) else  {
                return Just([]).eraseToAnyPublisher()
            }
            // get handle of native data task publisher
            let publisher = URLSession.shared.dataTaskPublisher(for: url)
                .handleEvents(
                    receiveSubscription: { _ in
                        activityIndicatorPublisher.send(true)
                    }, receiveCompletion: { _ in
                        activityIndicatorPublisher.send(false)
                    }, receiveCancel: {
                        activityIndicatorPublisher.send(false)
                    })
                .tryMap { data, response -> Data in
                    guard let httpResponse = response as? HTTPURLResponse,
                          httpResponse.statusCode == 200 else {
                              throw NetworkError.httpError
                          }
                    print(String(data: data, encoding: .utf8) ?? "")
                    return data
                }
                .decode(type: SearchUserResponse.self, decoder: JSONDecoder())
                .map { $0.items }
                .flatMap({ users in
                    var userViewModels = [UserViewModel]()
                    users.forEach { user in
                        userViewModels.append(contentsOf: UserViewModel(with: user))
                    }
                    return userViewModels
                })
                .catch { err -> Just<[UserViewModel]> in
                    print(err)
                    return Just([])
                }
                .eraseToAnyPublisher() // <-- HERE IS THE ERROR
            return publisher
        }

Solution

  • Unfortunately with those complex Combine pipelines sometimes compliler errors are displayed on the wrong line. In your case there are two problems, but not where the compiler is pointing.

    One being use of flatMap instead of map.

    This part of the pipeline:

    let publisher = URLSession.shared.dataTaskPublisher(for: url)
        .handleEvents(
            receiveSubscription: { _ in
                activityIndicatorPublisher.send(true)
            }, receiveCompletion: { _ in
                activityIndicatorPublisher.send(false)
            }, receiveCancel: {
                activityIndicatorPublisher.send(false)
            })
            .tryMap { data, response -> Data in
                 guard let httpResponse = response as? HTTPURLResponse,
                     httpResponse.statusCode == 200 else {
                     throw NetworkError.httpError
                 }
                 print(String(data: data, encoding: .utf8) ?? "")
                 return data
            }
            .decode(type: SearchUserResponse.self, decoder: JSONDecoder())
            .map { $0.items }
    

    returns Publisher<[User], Error>.

    Next you want to transforming that into Publisher<[UserViewModel], Error> for which you need a function map:

    func map<T>(_ transform: @escaping (Output) -> T) -> Publishers.Map<Upstream, T>
    

    which transforms one type of Output into another type of Output not flatMap:

    func flatMap<T, P>(maxPublishers: Subscribers.Demand = .unlimited, _ transform: @escaping (Self.Output) -> P) -> Publishers.FlatMap<P, Self> where T == P.Output, P : Publisher, Self.Failure == P.Failure
    

    which transforms Output into a new Publisher

    The second problem is with append(contentsOf;) which expects a Sequence of elements, in case of single elements you should use append(), but even simpler would be just to map the [User] to [UserViewModel]:

    { users in
        users.map { user in
            UserViewModel(with: user)
        }
    }
    

    so the whole function should work with those changes:

    static func searchUsers(query: String) -> AnyPublisher<[UserViewModel], Never> {
        // prepare URL
        let urlString = "\(baseURL)/search/users?q=\(query)"
        guard let url = URL(string: urlString) else  {
            return Just([]).eraseToAnyPublisher()
        }
        // get handle of native data task publisher
        let publisher = URLSession.shared.dataTaskPublisher(for: url)
            .handleEvents(
                receiveSubscription: { _ in
                    activityIndicatorPublisher.send(true)
                }, receiveCompletion: { _ in
                    activityIndicatorPublisher.send(false)
                }, receiveCancel: {
                    activityIndicatorPublisher.send(false)
                })
            .tryMap { data, response -> Data in
                guard let httpResponse = response as? HTTPURLResponse,
                      httpResponse.statusCode == 200 else {
                          throw NetworkError.httpError
                      }
                print(String(data: data, encoding: .utf8) ?? "")
                return data
            }
            .decode(type: SearchUserResponse.self, decoder: JSONDecoder())
            .map { $0.items }
            .map { users in
                users.map { user in
                    UserViewModel(with: user)
                }
            }
            .catch { err -> Just<[UserViewModel]> in
                print(err)
                return Just([])
            }
            .eraseToAnyPublisher()
        return publisher
    }