I perform many repeated requests in order to populate a field. I would like to cache the result and use the cached value the next time around.
public func getItem(_ id: String) -> AnyPublisher<Item?, Never> {
if let item = itemCache[id] {
return Just(item).eraseToAnyPublisher()
}
return downloadItem(id: id)
.map { item in
if let item = item {
itemCache[id] = item
}
return item
}
.eraseToAnyPublisher()
}
}
func downloadItem(_ id: String) -> AnyPublisher<Item?, Never> { ... }
And this is called like this:
Just(["a", "a", "a"]).map(getItem)
However, all the requests are calling downloadItem. downloadItem does return on the main queue. I also tried wrapping the entire getItem function into Deferred but that had the same result.
First, the issue was that the function is being evaluated and only a publisher is returned. So the cache check is evaluated each time before the network publisher is ever subscribed to. Using Deferred is the proper fix for that. However, that still didn't solve the problem.
The solution was instead to first cache a shared publisher while the network request is pending so all requests during the network call will use the same publisher, then when it's complete to cache a Just publisher for the all future calls:
public func getItem(_ id: String) -> AnyPublisher<Item?, Never> {
if let publisher = self.publisherCache[id] {
return publisher
}
let publisher = downloadItem(id)
.handleEvents(receiveOutput: {
// Re-cache a Just publisher once the network request finishes
self.publisherCache[id] = Just($0).eraseToAnyPublisher()
})
.share() // Ensure the same publisher is returned from the cache
.eraseToAnyPublisher()
// Cache the publisher to be used while downloading is in progress
self.publisherCache[id] = publisher
return publisher
}
One note, is that downloadItem(id) is async and being recieved on the main loop. When I replaced downloadItem(id) with Just(Item()) for testing, this didn't work beause the entire publisher chain was evaluated on creation. Use Just(Item()).recieve(on: Runloop.main) to fix that while testing.
Related
As example I have a basic published value like
#Published var value: String
I have want to validates this value of my form to give my user an output. For that I will use Combine in my MVVM project.
Now this type of value needs to be validated against my REST API. For my REST API I already have a method to get my results of my like getFirstMailboxRedirectFromAPI which returns AnyPublisher<MailboxAPIResponse, APIError>. MailboxAPIResponse is a decodable object for the api response. So if I just want to display the result, I create a subscriber with .sink add the result to a variable which will be shown in a view. So good so far. Now my problem:
As described in the first section my value is already a Publisher (because of #Published), where I can do some .map stuff for validation with it and returning true or false if everything is fine or not.
So to validate my published value I need to call my other Publisher which uses the API to check if the value already exists. But I don't know how this should work.
This is my code so far but this doesn't work. But this shows you my idea how it should work.
private var isMailboxRedirectNameAvailablePublisher: AnyPublisher<Bool, Never> {
$mailboxRedirectName
.debounce(for: 0.5, scheduler: RunLoop.main)
.setFailureType(to: Error.self)
.flatMap { name in
self.getFirstMailboxRedirectFromAPI(from: name, and: self.domainName)
.map { apiResponse in
return apiResponse.response.data.count == 0
}
}
.eraseToAnyPublisher()
}
So in result the publisher should use the redirectName to call the API and the API gives me the result if the mailbox already exists, then returns a boolean, if it's existing or not.
How can I nest multiple publishers and use the result of the API publisher in my published value publisher?
I simplified a little but key take aways are 1) use switchToLatest to flatten a Publisher of Publishers if you want the operation to restart (flatMap is a merge, so events could arrive out of order). 2) You need to handle the failure types and make sure an inner Publisher never fails or the outer publisher will also complete.
final class M: ObservableObject {
#Published var mailboxRedirectName: String = ""
private var isMailboxRedirectNameAvailablePublisher: AnyPublisher<Bool, Never> {
$mailboxRedirectName
.debounce(for: 0.5, scheduler: DispatchQueue.main)
.map { [weak self] name -> AnyPublisher<Bool, Never> in
guard let self = self else { return Just(false).eraseToAnyPublisher() }
return self
.getFirstMailboxRedirectFromAPI(from: name)
.replaceError(with: false)
.eraseToAnyPublisher()
}
.switchToLatest()
.eraseToAnyPublisher()
}
func getFirstMailboxRedirectFromAPI(from name: String) -> AnyPublisher<Bool, Error> {
Just(true).setFailureType(to: Error.self).eraseToAnyPublisher()
}
}
Here is a pseudo code of what I need to achieve:
func apiRequest1() -> Future<ResultType1, Error> { ... }
func apiRequest2() -> Future<ResultType2, Error> { ... }
func transform(res1: ResultType1, res2: ResultType2) -> ResultType3 { ... }
func combinedApiRequests() -> Future<ResultType3, Error> {
(resultType1, resultType2) = execute apiRequest1() and apiRequest2() asynchronously
resultType3 = transform(resultType1, resultType2)
return a Future publisher with resultType3
}
How would combinedApiRequests() look?
There's no need to return a Future publisher. Future publisher is a specific publisher, but as far as a downstream is concerned, a publisher is defined by its output and failure types. Instead, return a AnyPublisher<ResultType3, Error>.
Zip is a publisher that waits for all results to arrive to emit a value. This is probably what you'd need (more on this later). This is how your function could look:
func combinedApiRequests() -> AnyPublisher<ResultType3, Error> {
Publishers.Zip(apiRequest1, apiRequest2)
.map { transform(res1: $0, res2: $1) }
.eraseToAnyPublisher()
}
There is also CombineLatest publisher. For the first result from each upstream, it behaves the same as Zip, but for subsequent results it differs. In your case, it doesn't matter since Future is a one-shot publisher, but if the upstream publishers emitted multiple values, then you'd have to decide for your specific use case whether to use Zip - which always waits for all upstreams to emit a value before it emits a combined value, or CombineLatest - which emits with each new upstream value and combines it with the latest for other upstreams.
I've created a Combine publisher chain that looks something like this:
let pub = getSomeAsyncData()
.mapError { ... }
.map { ... }
...
.flatMap { data in
let wsi = WebSocketInteraction(data, ...)
return wsi.subject
}
.share().eraseToAnyPublisher()
It's a flow of different possible network requests and data transformations. The calling code wants to subscribe to pub to find out when the whole asynchronous process has succeeded or failed.
I'm confused about the design of the flatMap step with the WebSocketInteraction. That's a helper class that I wrote. I don't think its internal details are important, but its purpose is to provide its subject property (a PassthroughSubject) as the next Publisher in the chain. Internally the WebSocketInteraction uses URLSessionWebSocketTask, talks to a server, and publishes to the subject. I like flatMap, but how do you keep this piece alive for the lifetime of the Publisher chain?
If I store it in the outer object (no problem), then I need to clean it up. I could do that when the subject completes, but if the caller cancels the entire publisher chain then I won't receive a completion event. Do I need to use Publisher.handleEvents and listen for cancellation as well? This seems a bit ugly. But maybe there is no other way...
.flatMap { data in
let wsi = WebSocketInteraction(data, ...)
self.currentWsi = wsi // store in containing object to keep it alive.
wsi.subject.sink(receiveCompletion: { self.currentWsi = nil })
wsi.subject.handleEvents(receiveCancel: {
wsi.closeWebSocket()
self.currentWsi = nil
})
Anyone have any good "design patterns" here?
One design I've considered is making my own Publisher. For example, instead of having WebSocketInteraction vend a PassthroughSubject, it could conform to Publisher. I may end up going this way, but making a custom Combine Publisher is more work, and the documentation steers people toward using a subject instead. To make a custom Publisher you have to implement some of things that the PassthroughSubject does for you, like respond to demand and cancellation, and keep state to ensure you complete at most once and don't send events after that.
[Edit: to clarify that WebSocketInteraction is my own class.]
It's not exactly clear what problems you are facing with keeping an inner object alive. The object should be alive so long as something has a strong reference to it.
It's either an external object that will start some async process, or an internal closure that keeps a strong reference to self via self.subject.send(...).
class WebSocketInteraction {
private let subject = PassthroughSubject<String, Error>()
private var isCancelled: Bool = false
init() {
// start some async work
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if !isCancelled { self.subject.send("Done") } // <-- ref
}
}
// return a publisher that can cancel the operation when
var pub: AnyPublisher<String, Error> {
subject
.handleEvents(receiveCancel: {
print("cancel handler")
self.isCancelled = true // <-- ref
})
.eraseToAnyPublisher()
}
}
You should be able to use it as you wanted with flatMap, since the pub property returned publisher, and the inner closure hold a reference to self
let pub = getSomeAsyncData()
...
.flatMap { data in
let wsi = WebSocketInteraction(data, ...)
return wsi.pub
}
I know in general a publisher is more powerful than a closure, however I want to ask and discuss a specific example:
func getNotificationSettingsPublisher() -> AnyPublisher<UNNotificationSettings, Never> {
let notificationSettingsFuture = Future<UNNotificationSettings, Never> { (promise) in
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
promise(.success(settings))
}
}
return notificationSettingsFuture.eraseToAnyPublisher()
}
I think this is a valid example of a Future publisher and it could be used here instead of using a completion handler. Let's do something with it:
func test() {
getNotificationSettingsPublisher().sink { (notificationSettings) in
// Do something here
}
}
This works, however it will tell me that the result of sink (AnyCancellable) is unused. So whenever I try to get a value, I need to either store the cancellable or assign it until I get a value.
Is there something like sinkOnce or an auto destroy of cancellables? Sometimes I don't need tasks to the cancelled. I could however do this:
func test() {
self.cancellable = getNotificationSettingsPublisher().sink { [weak self] (notificationSettings) in
self?.cancellable?.cancel()
self?.cancellable = nil
}
}
So once I receive a value, I cancel the subscription. (I could do the same in the completion closure of sink I guess).
What's the correct way of doing so? Because if I use a closure, it will be called as many times as the function is called, and if it is called only once, then I don't need to cancel anything.
Would you say normal completion handlers could be replaced by Combine and if so, how would you handle receiving one value and then cancelling?
Last but not least, the completion is called, do I still need to cancel the subscription? I at least need to update the cancellable and set it to nil right? I assume storing subscriptions in a set is for long running subscriptions, but what about single value subscriptions?
Thanks
Instead of using the .sink operator, you can use the Sink subscriber directly. That way you don't receive an AnyCancellable that you need to save. When the publisher completes the subscription, Combine cleans everything up.
func test() {
getNotificationSettingsPublisher()
.subscribe(Subscribers.Sink(
receiveCompletion: { _ in },
receiveValue: ({
print("value: \($0)")
})
))
}
I am using rxSwift and I have a dictionary of observables which can be subscribed to.
var observables: [String: Observable<Bool>] = [:]
At some point I have to clean up some of those observables. I do it as follows
observables.removeValue(forKey: someKey)
Is it enough to clean up the observables with the above line? Is the observable also killed (or how do I "kill" it)? Someone might already be subscribed to the observable and then even after removing it from the dictionary it would still be alive and might fire, right?
Or is the observable gone the moment I remove it because nobody holds a strong reference to it? What happens in the moment the observable is removed to potential subsribers?
I do not have access to the subscribers from the class where the dictionary with the observables is kept.
You can use takeUntil operator. It will send a completed signal to the observable, so the subscriber will release the retained resources.
For example, you can setup a PublishSubject where you send the observable identifier to complete that observable.
var observables: [String: Observable<Bool>] = [:]
let finishObservable = PublishSubject<String>()
func addObservable(observable: Observable<Bool>, withId identifier: String) -> Observable<Bool> {
let condition = finishObservable.filter { $0 == identifier }
let newObservable = observable.takeUntil(condition)
observables[identifier] = newObservable
return newObservable
}
This way, to clean an observable, you send the observable identifier and then you can remove the completed sequence from the dictionary.
func removeObservable(identifier: String) {
// Complete the observable so it stops sending events and subscriber releases resources
finishObservable.onNext(identifier)
observables.removeValue(forKey: identifier)
}
If you're planning to share subscription between observers, you can also use a ConnectableObservable. I've used this kind of observables when subscribers come and go but you want to share the same subscription. It's usefull if the observable fetches network resources for example.
var disposables: [String: Disposable] = [:]
func addObservable(observable: Observable<Bool>, withId identifier: String) -> Observable<Bool> {
let newObservable: ConnectableObservable = observable.replay(1)
disposables[identifier] = newObservable.connect() // This call triggers the subscription, so you can call it later
return newObservable
}
func removeObservable(identifier: String) {
if let disposable = disposables.removeValue(forKey: identifier) {
disposable.dispose()
}
}