使用 Combine 的具有通用 ReturnType 的 FlatMap

FlatMap with Generic ReturnType using Combine

我正在构建网络 API。 我是 Combine 的新手,我遇到了一些麻烦,我正在尝试链接发布网络请求,在这种情况下,我正在形成一个 URLRequest 发布者并将其分派给另一个发布者,问题是我不能flatMap 在第二个发布者上工作。

首先我 assemble URLRequest 与 Auth 令牌:

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()
    }

然后我试图发送一个请求(发布者)和 return 另一个发布者,问题是 .flatMap 没有被调用:

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()

}

未调用的最终发布者如下:

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()
    }
}

运行代码:

 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)

我把你的代码归结为 Combine 部分。我无法重现您描述的问题。我将 post 下面的代码。我建议您开始一次简化您的代码,看看是否有帮助。分解出 Auth 和 Facebook 令牌代码似乎是一个很好的开始。另一个好的调试技术可能是放入更明确的类型声明,以确保您的闭包接受并返回您期望的内容。 (就在前几天,我有一个 map,当我真正映射到 Optional 时,我认为我正在应用于数组)。

这里是游乐场:

导入 UIKit 导入组合

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)

我重构了你的代码。将有问题的方法分解为几个函数。我找不到任何问题。下面是我的重构。您会注意到,我将所有构造事物的代码分解为它们自己的函数,因此无需处理效果即可轻松测试它们(我什至不必模拟效果来测试逻辑。)

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 { }