Combine Future Publisher方法提前结束,如何确保Token更新后返回请求?
The core issue here is that Combine's asynchronous operations (like URLSession.dataTask) don't block the calling thread, so your sink closure runs after prepareURLRequest has already returned the unmodified request when verify() returns true. The sleep(2) in the false case works only because it's a synchronous block that forces the thread to wait—this is not a good pattern to rely on, either.
To fix this properly, we need to refactor the code to embrace Combine's asynchronous nature instead of trying to force synchronous behavior. Here's how to adjust it:
Step-by-Step Solution
- Change
prepareURLRequestto return a Publisher instead of a directURLRequest. This lets us chain the token retrieval and request modification as part of an asynchronous stream. - Remove synchronous blocking (like
sleep(2)—if your keychain access is actually synchronous, you can run it directly in theFuture, but prefer async APIs where possible). - Chain the token retrieval to request modification within the Publisher pipeline.
Modified Code
import Cocoa import Combine func prepareURLRequest(for url: URL) -> AnyPublisher<URLRequest, Never> { let baseRequest = URLRequest(url: url) let subscription = Token() return subscription.getToken() .map { token in var authenticatedRequest = baseRequest // Attach the received token to the request (e.g., add an Authorization header) authenticatedRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") print("Token attached to request: \(token)") return authenticatedRequest } .handleEvents(receiveSubscription: { _ in print("Starting token retrieval...") }, receiveCompletion: { _ in print("Token retrieval completed") }) } class Token { let token = PassthroughSubject<String,Never>() func verify() -> Bool { // TODO: Token verification logic Bool.random() } func getToken() -> AnyPublisher<String,Never> { return Future<String, Never> { promise in if self.verify() { let url = URLRequest(url: URL(string: "http://avatars.io/twitter/twostraws")!) URLSession.shared.dataTask(with: url) { data, response, error in print("Data Task started") if let error = error { print("Error: \(error)") // In a real app, you'd handle errors properly (adjust Publisher failure type if needed) promise(.success("error-fallback-token")) return } guard let response = response as? HTTPURLResponse else { print("Invalid response") promise(.success("invalid-response-fallback")) return } print("Data task received response with code: \(response.statusCode)") promise(.success("\(response.statusCode)")) }.resume() } else { print("Using keychain token") // Simulate keychain access (replace with actual async keychain API if available) DispatchQueue.global().asyncAfter(deadline: .now() + 2) { promise(.success("keychain-token-success")) } } }.eraseToAnyPublisher() } } // Usage: Subscribe to the Publisher to get the authenticated request let url = URL(string: "http://avatars.io/twitter/twostraws")! let requestSubscription = prepareURLRequest(for: url) .sink(receiveValue: { authenticatedRequest in print("Received authenticated request: \(authenticatedRequest)") // Use the request here (e.g., make the API call) })
Key Changes Explained
- Publisher Return Type:
prepareURLRequestnow returnsAnyPublisher<URLRequest, Never>, which means it emits a single authenticated request once the token is ready. - Async Keychain Access: Replaced
sleep(2)withDispatchQueue.global().asyncAfterto simulate asynchronous keychain access (in a real app, use async keychain APIs to avoid blocking threads). - Request Modification in
map: We usemapto transform the retrieved token into a modifiedURLRequest—this ensures the token is attached before the request is emitted to the subscriber. - Proper Async Flow: The calling code subscribes to the Publisher, so it only gets the request once the token is fully retrieved, regardless of whether
verify()returnstrueorfalse.
Why Your Original Code Failed
When verify() returns true, URLSession.dataTask runs asynchronously on a background thread. The sink closure is scheduled to run on the main thread (or the current RunLoop) after prepareURLRequest has finished executing and returned the base request. The sleep(2) in the false case worked only because it blocked the current thread, forcing the Future to resolve before prepareURLRequest continued—but this is a blocking anti-pattern that can freeze your UI if run on the main thread.
内容的提问来源于stack exchange,提问作者Paweł Madej

