I'm building a network API. I'm new to Combine and I'm having some troubles with it, I'm trying to chain publish network requests, in this case I'm forming an URLRequest publisher and dispatching it on another publisher, the problem is that I cant make the flatMap work on the second publisher.
First I assemble the URLRequest
with the Auth token:
func asURLRequest(baseURL: String) -> AnyPublisher<URLRequest, NetworkRequestError> {
return Deferred {
Future<URLRequest, NetworkRequestError> { promise in
if var urlComponents = URLComponents(string: baseURL) {
urlComponents.path = "\(urlComponents.path)\(path)"
urlComponents.queryItems = queryItemsFrom(params: queryParams)
if let finalURL = urlComponents.url {
if let user = Auth.auth().currentUser {
print("##### final url -> \(finalURL)")
// Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
user.getIDToken(completion: { (token, error) in
if let fbToken = token {
var request = URLRequest(url: finalURL)
request.httpMethod = method.rawValue
request.httpBody = requestBodyFrom(params: body)
let defaultHeaders: HTTPHeaders = [
HTTPHeaderField.contentType.rawValue: contentType.rawValue,
HTTPHeaderField.acceptType.rawValue: contentType.rawValue,
HTTPHeaderField.authentication.rawValue: fbToken
]
request.allHTTPHeaderFields = defaultHeaders.merging(headers ?? [:], uniquingKeysWith: { (first, _) in first })
print("##### API TOKEN() SUCCESS: \(defaultHeaders)")
promise(.success(request))
}
if let fbError = error {
print("##### API TOKEN() ERROR: \(fbError)")
promise(.failure(NetworkRequestError.decodingError))
}
})
}
} else {
promise(.failure(NetworkRequestError.decodingError))
}
} else {
promise(.failure(NetworkRequestError.decodingError))
}
}
}.eraseToAnyPublisher()
}
Then I'm trying to dispatch a request (publisher) and return another publisher, the problem is that the .flatMap is not getting called:
struct APIClient {
var baseURL: String!
var networkDispatcher: NetworkDispatcher!
init(baseURL: String,
networkDispatcher: NetworkDispatcher = NetworkDispatcher()) {
self.baseURL = baseURL
self.networkDispatcher = networkDispatcher
}
/// Dispatches a Request and returns a publisher
/// - Parameter request: Request to Dispatch
/// - Returns: A publisher containing decoded data or an error
func dispatch<R: Request>(_ request: R) -> AnyPublisher<R.ReturnType, NetworkRequestError> {
print("##### --------> \(request)")
//typealias RequestPublisher = AnyPublisher<R.ReturnType, NetworkRequestError>
return request.asURLRequest(baseURL: baseURL)
.flatMap { request in
//NOT GETTING CALLED
self.networkDispatcher.dispatch(request: request)
}.eraseToAnyPublisher()
}
The final publisher that is not being called is the following:
struct NetworkDispatcher {
let urlSession: URLSession!
public init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}
/// Dispatches an URLRequest and returns a publisher
/// - Parameter request: URLRequest
/// - Returns: A publisher with the provided decoded data or an error
func dispatch<ReturnType: Codable>(request: URLRequest) -> AnyPublisher<ReturnType, NetworkRequestError> {
return urlSession
.dataTaskPublisher(for: request)
// Map on Request response
.tryMap({ data, response in
// If the response is invalid, throw an error
if let response = response as? HTTPURLResponse,
!(200...299).contains(response.statusCode) {
throw httpError(response.statusCode)
}
// Return Response data
return data
})
// Decode data using our ReturnType
.decode(type: ReturnType.self, decoder: JSONDecoder())
// Handle any decoding errors
.mapError { error in
handleError(error)
}
// And finally, expose our publisher
.eraseToAnyPublisher()
}
}
Running the code:
struct ReadUser: Request {
typealias ReturnType = UserData
var path: String
var method: HTTPMethod = .get
init(_ id: String) {
path = "users/\(id)"
}
}
let apiClient = APIClient(baseURL: BASE_URL)
var cancellables = [AnyCancellable]()
apiClient.dispatch(ReadUser(Auth.auth().currentUser?.uid ?? ""))
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { result in
switch result {
case .failure(let error):
// Handle API response errors here (WKNetworkRequestError)
print("##### Error loading data: \(error)")
default: break
}
},
receiveValue: { value in
})
.store(in: &cancellables)
CodePudding user response:
I took your code and boiled it down to just the Combine parts. I could not reproduce the issue you are describing. I'll post that code below. I recommend you start simplifying your code a bit at a time to see if that helps. Factoring out the Auth and Facebook token code seems like a good candidate to start with. Another good debugging technique might be to put in more explicit type declarations to make sure your closures are taking and returning what you expect. (just the other day I had a map
that I thought I was applying to an Array when I was really mapping over Optional).
Here's the playground:
import UIKit import Combine
func asURLRequest(baseURL: String) -> AnyPublisher<URLRequest, Error> {
return Deferred {
Future<URLRequest, Error> { promise in
promise(.success(URLRequest(url: URL(string: "https://www.apple.com")!)))
}
}.eraseToAnyPublisher()
}
struct APIClient {
var networkDispatcher: NetworkDispatcher!
init(networkDispatcher: NetworkDispatcher = NetworkDispatcher()) {
self.networkDispatcher = networkDispatcher
}
func dispatch() -> AnyPublisher<Data, Error> {
return asURLRequest(baseURL: "Boo!")
.flatMap { (request: URLRequest) -> AnyPublisher<Data, Error> in
print("Request Received. \(String(describing: request))")
return self.networkDispatcher.dispatch(request: request)
}.eraseToAnyPublisher()
}
}
func httpError(_ code: Int) -> Error {
return NSError(domain: "Bad Things", code: -1, userInfo: nil)
}
func handleError(_ error: Error) -> Error {
debugPrint(error)
return error
}
struct NetworkDispatcher {
let urlSession: URLSession!
public init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}
func dispatch(request: URLRequest) -> AnyPublisher<Data, Error> {
return urlSession
.dataTaskPublisher(for: request)
.tryMap({ data, response in
if let response = response as? HTTPURLResponse,
!(200...299).contains(response.statusCode) {
throw httpError(response.statusCode)
}
// Return Response data
return data
})
.mapError { error in
handleError(error)
}
.eraseToAnyPublisher()
}
}
let apiClient = APIClient()
var cancellables = [AnyCancellable]()
apiClient.dispatch()
.print()
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { result in
debugPrint(result)
switch result {
case .failure(let error):
// Handle API response errors here (WKNetworkRequestError)
print("##### Error loading data: \(error)")
default: break
}
},
receiveValue: { value in
debugPrint(value)
})
.store(in: &cancellables)
CodePudding user response:
I refactored your code. Breaking down the offending method into several functions. I could not find any problem. Below is my refactoring. You will notice that I broke all the code that constructs things into their own functions so they can be easily tested without dealing with the effect (I don't even have to mock the effect to test the logic.)
extension Request {
func asURLRequest(baseURL: String) -> AnyPublisher<URLRequest, NetworkRequestError> {
guard let user = Auth.auth().currentUser else {
return Fail(error: NetworkRequestError.missingUser)
.eraseToAnyPublisher()
}
return user.idTokenPublisher()
.catch { error in
Fail(error: NetworkRequestError.badToken(error))
}
.tryMap { token in
makeRequest(
finalURL: try finalURL(baseURL: baseURL),
fbToken: token
)
}
.eraseToAnyPublisher()
}
func finalURL(baseURL: String) throws -> URL {
guard var urlComponents = URLComponents(string: baseURL) else {
throw NetworkRequestError.malformedURLComponents
}
urlComponents.path = "\(urlComponents.path)\(path)"
urlComponents.queryItems = queryItemsFrom(params: queryParams)
guard let result = urlComponents.url else {
throw NetworkRequestError.malformedURLComponents
}
return result
}
func makeRequest(finalURL: URL, fbToken: String) -> URLRequest {
var request = URLRequest(url: finalURL)
request.httpMethod = method.rawValue
request.httpBody = requestBodyFrom(params: body)
let defaultHeaders: HTTPHeaders = [
HTTPHeaderField.contentType.rawValue: contentType.rawValue,
HTTPHeaderField.acceptType.rawValue: contentType.rawValue,
HTTPHeaderField.authentication.rawValue: fbToken
]
request.allHTTPHeaderFields = defaultHeaders.merging(
headers ?? [:],
uniquingKeysWith: { (first, _) in first }
)
return request
}
}
extension User {
func idTokenPublisher() -> AnyPublisher<String, Error> {
Deferred {
Future { promise in
getIDToken(completion: { token, error in
if let token = token {
promise(.success(token))
}
else {
promise(.failure(error ?? UnknownError()))
}
})
}
}
.eraseToAnyPublisher()
}
}
struct UnknownError: Error { }