I have a somewhat complicated architecture for a feature in my app.
Sample code is below. My original expectation was that this would only print once, because I call cancellableSet.removeAll(). But this actually ends up being called twice, which creates problems in my application.
How do I get this so it only fires what's in the sink after the subscription is stored in the cancellable set.
Note that I have a few restrictions here that I'll mention. My sample code is just simplifying this.
Can't use a take or drop operation, as this may get called an undetermined amount of times.
import Combine
enum State {
case loggedOut
case doingSomething
}
let aState = CurrentValueSubject<State, Never>(.doingSomething)
private var cancellableSet: Set<AnyCancellable> = []
func logUserOut() {
cancellableSet.removeAll()
aState.send(.loggedOut)
}
func doSomethingElse() { }
aState.sink { newState in
print("numberOfSubscriptions is: \(cancellableSet.count)")
switch newState {
case .loggedOut:
doSomethingElse()
case .doingSomething:
logUserOut()
}
}
.store(in: &cancellableSet)
The problem in your code is that the subscription starts delivering values synchronously before the call to sink returns, and so before the call to store even begins.
One way to solve this is to turn aState into a ConnectablePublisher before subscribing. A ConnectablePublisher doesn't publish until its connect method is called. So call connect after store returns.
You can use the makeConnectable method on any Publisher whose Failure == Never to wrap it in a ConnectablePublisher.
let connectable = aState.makeConnectable()
connectable.sink { newState in
print("numberOfSubscriptions is: \(cancellableSet.count)")
switch newState {
case .loggedOut:
doSomethingElse()
case .doingSomething:
logUserOut()
}
}
.store(in: &cancellableSet)
connectable.connect()
If the queue this code is being run on is a serial one, then maybe you can move the execution of the code inside the sink to the end of the queue. This way, the program will find the time to store the subscription in the set.
aState.sink { newState in
DispatchQueue.main.async { // or whatever other queue you are running on
print("numberOfSubscriptions is: \(cancellableSet.count)")
switch newState {
case .loggedOut:
doSomethingElse()
case .doingSomething:
logUserOut()
}
}
}
.store(in: &cancellableSet)
It's a bit dirty tho.
Related
I'm trying to convert some combine friendly code to async/await. For this post I'll use a simple example cut down from some real code just to keep the discussion simple.
import Combine
import Foundation
enum State {
case a
case b
case c
}
class StateMachine: Publisher {
typealias Output = State
typealias Failure = Error
private let currentState = CurrentValueSubject<State, Error>(State.a)
func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
currentState.receive(subscriber: subscriber)
}
func gotoB() { currentState.value = .b }
func gotoC() { currentState.value = .c }
}
let sm = StateMachine()
let c = sm.sink {
print("Failed with \($0)")
}
receiveValue: {
print("New value \($0)")
}
sm.gotoB()
sm.gotoC()
So in this code (which works in a playground) I want the StateMachine to act as a publisher so it can be subscribed to directly. To achieve this I use an internal CurrentValueSubject and forward the publisher's receive(subscriber:) to it.
However when I change class StateMachine: to actor StateMachine:
actor StateMachine: Publisher {
//...
}
// ...
await sm.gotoB()
await.sm.gotoC()
this code no longer compiles and throws these errors:
expression failed to parse:
error: SwiftFormat.playground:18:10: error: actor-isolated instance method 'receive(subscriber:)' cannot be used to satisfy nonisolated protocol requirement
func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
^
SwiftFormat.playground:18:10: note: add 'nonisolated' to 'receive(subscriber:)' to make this instance method not isolated to the actor
func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
^
nonisolated
Combine.Publisher:5:10: note: 'receive(subscriber:)' declared here
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
^
Now I can add nonisolated as suggested, but I'm not sure that isn't going to break the concurrency handling that actor is introducing because there's nothing to stop concurrent access to the internal subject.
So is there a way to keep the type as a Combine Publisher or is there some other async/away approach I've just not read about which is now the preferred approach?
The first thing to ask here is "what is it that is asynchronous?"
I don't think in this case that the gotoA or gotoB are asynchronous. The thing that is asynchronous is the subscription to the current value.
The consumers will subscribe to it and then at some point later in time it will change and they are updated.
In async await this can be modelled as an AsyncStream.
To you an async stream you access it something like:
for await state in stateMachine.values {
print("received value \(state)")
}
So to make this work we need to make stateMachine give out an AsyncStream.
Something like this would work...
enum State {
case a
case b
case c
}
class StateMachine {
var subscriptions: [UUID: AsyncStream<State>.Continuation] = [:] // 1.
var currentState = State.a {
didSet {
subscriptions.allValues.forEach {
$0.yield(currentState)
}
}
} // 2.
var values: AsyncStream<State> {
AsyncStream { continuation in
let id = UUID()
subscriptions[id] = continuation
continuation.yield(currentValue)
continuation.onTermination = { [weak self] _ in
self?.subscriptions.removeValue(forKey: id)
}
}
} // 3.
func gotoB() {
currentState = .b
}
}
In this we...
Create a place to store any incoming subscriptions
Make sure that when current state changes we update all the subscribers
When a new subscriber is added we give it an AsyncStream to listen to
This is the beginnings of what you would need to create. You might need to use some sort of Singleton access to this if you have multiple places in your app that need to subscribe to the same state.
But this should give you the initial idea. We use something very similar to this in the app I am currently working on. Sometimes to create a subscribable cache. Sometimes to respond to error messages, etc...
I'm trying to write an integration test for a Reactor in an app built with ReactorKit and Realm/RxRealm.
I'm having trouble using TestScheduler to simulate user actions and test the expected emitted states.
In a nutshell, my problem is this: I'm binding an action that will make my Reactor save an item to Realm, my Reactor also observes changes to this object in Realm, and I expect my Reactor to emit the new state of this item observed from Realm.
What I'm seeing is that my test does not get the emission of the newly saved object in time to assert its value, it's emitted after my test assertion runs.
There is a fair amount of code involved, but attempting to whittle it down into a self-contained example of what it all roughly looks like below:
struct MyObject {
var counter: Int = 0
}
class MyReactor: Reactor {
enum Action {
case load
case mutateState
}
enum Mutation {
case setObject(MyObject)
}
struct State {
var object: MyObject?
}
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .load:
return service.monitorObject().map(Mutation.setObject)
case .mutateState:
guard var myObject = currentState.object else { return .empty() }
myObject.counter += 1
return service.save(myObject).andThen(.empty())
}
}
func reduce(state: State, mutation: Mutation) -> Observable<State> {
var newState = state
switch mutation {
case let .setObject(object):
// Should be called twice in tests, once on load, once after mutateState action
newState.object = object
}
}
}
struct Service {
// There is always at least one default instance of `MyObject` in Realm.
func monitorObject() -> Observable<MyObject> {
return Observable
.collection(from: realm.objects(MyObject.self))
.map { $0.first! }
}
func save(_ object: MyObject) -> Completable {
return Completable.create { emitter in
try! realm.write {
realm.add(object, update: .modified)
}
emitter(.completed)
return Disposables.create()
}
}
}
class MyTest: QuickSpec {
var scheduler: TestScheduler!
var sut: MyReactor!
var disposeBag: DisposeBag!
var service: Service!
var config: Realm.Configuration!
override func spec() {
beforeEach {
config = Realm.Configuration(inMemoryIdentifier: UUID().uuidString)
scheduler = TestScheduler(initialClock: 0)
disposeBag = DisposeBag()
sut = MyReactor()
service = Service(realmConfig: config)
}
describe("when my reactor gets a mutateState action") {
it("should mutate state") {
scheduler.createHotObservable([
.next(1, Action.load),
.next(2, Action.mutateState),
])
.bind(to: sut.action)
.disposed(by: disposeBag)
let response = scheduler.start(created: 0, subscribed: 0, disposed: 1000) {
sut.state.map(\.object)
}
// Counter always equals 0
XCTAssertTrue(response.events.last!.value.element!!.counter == 1)
}
}
}
}
What I'm expecting to happen is my Reactor's state is set for a 2nd time, before the XCTAssertTrue is hit. What is actually happening is the assert is hit with the initially loaded state, and then, my reactor's state is set again.
I thought my problem might be related to schedulers. Something I tried was injecting the test scheduler into my Service and doing observeOn(testScheduler) on my monitorObject function. But I'm still observing the assert get hit before the reactor's state is set for the 2nd time. I'm also not sure if a nuance of RxRealm/Realm change set notifications is the cause - not sure how to verify whether that might be the case.
Hopefully the problem and question is clear. Thanks in advance for any help.
I decided attempting to write an integration test was more trouble than it was worth and probably not going to result in very useful tests anyway.
So you are trying to test to see if Realm works. I don't use Realm, but based on your description, it probably updates the object on an internal thread and then you get the emission on a subsequent cycle.
You can test it by using an XCTestExpectation. Here is documentation from Apple: https://developer.apple.com/documentation/xctest/asynchronous_tests_and_expectations/testing_asynchronous_operations_with_expectations
Note however, that if something goes wrong in Realm and this test fails, there isn't anything you can do about it.
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)")
})
))
}
While making a thread-safe Singleton, it is advised to use a sync for read and an async with a barrier for write operation.
My question is why do we use a sync for read? What might happen if we perform a read with async operation?
Here is an example of what is recommended:
func getUser(id: String) throws -> User {
var user: User!
try concurrentQueue.sync {
user = try storage.getUser(id)
}
return user
}
func setUser(_ user: User, completion: (Result<()>) -> Void) {
try concurrentQueue.async(flags: .barrier) {
do {
try storage.setUser(user)
completion(.value(())
} catch {
completion(.error(error))
}
}
}
The concept of using concurrent queue with “read concurrently with sync; write with barrier with async” is a very common synchronization pattern called “reader-writer”. The idea is that the concurrent queue is just for synchronizing writes with a barrier, but that reads will take place concurrently with respect to other reads.
So, here’s a simple, real-world example of using reader-writer for synchronized access to some private state property:
enum State {
case notStarted
case running
case complete
}
class ComplexProcessor {
private var readerWriterQueue = DispatchQueue(label: "...", attributes: .concurrent)
// private backing stored property
private var _state: State = .notStarted
// exposed computed property synchronizes access using reader-writer pattern
var state: State {
get { readerWriterQueue.sync { _state } }
set { readerWriterQueue.async { self._state = newValue } }
}
func start() {
state = .running
DispatchQueue.global().async {
// do something complicated here
self.state = .complete
}
}
}
Consider:
let processor = ComplexProcessor()
processor.start()
And then, later:
if processor.state == .complete {
...
}
The state computed property is using the reader-writer pattern to offer thread-safe access to the underlying stored property. It synchronizes access to some memory location, and we are confident that it will be responsive. In this case, we don’t need confusing #escaping closures: The sync reads result in very simple code that is easy to reason about.
That having been said, in your example, you’re not just synchronizing interaction with some property, but synchronizing the interaction with storage. If that’s local storage that is guaranteed to be responsive, then the reader-writer pattern is probably fine.
But if storage methods could take anything more than a few milliseconds to run, you wouldn’t want to use the reader-writer pattern. The fact that getUser can throw errors makes me wonder if storage is already doing complicated processing. And even if it is just reading quickly from some local store, what if it was later refactored to interact with some remote store, subject to unknown network latency/issues? Bottom line, it is questionable to have the getUser method making assumptions about implementation details of storage, assuming that the value will always be returned quickly.
In that case, you would refactor getUser method to use #escaping completion handler closure, as suggested by Jeffery Thomas. We never want to have a synchronous method that might take more than a few milliseconds, because we never want to block the calling thread (especially if it’s the main thread).
By the way, if you stay with reader-writer pattern, you can simplify your getUser, because sync returns whatever value its closure returns:
func getUser(id: String) throws -> User {
return try concurrentQueue.sync {
try storage.getUser(id)
}
}
And you can’t use try in conjunction with async (only within your do-catch block). So it’s just:
func setUser(_ user: User, completion: (Result<()>) -> Void) {
concurrentQueue.async(flags: .barrier) {
do {
try storage.setUser(user)
completion(.value(())
} catch {
completion(.error(error))
}
}
}
It's all in what you want. By changing get user to async, then you need to use a callback to wait for the value.
func getUser(id: String, completion: #escaping (Result<User>) -> Void) -> Void {
concurrentQueue.async {
do {
let user = try storage.getUser(id)
completion(.value(user))
} catch {
completion(.error(error))
}
}
}
func setUser(_ user: User, completion: #escaping (Result<()>) -> Void) {
concurrentQueue.async(flags: .barrier) {
do {
try storage.setUser(user)
completion(.value(()))
} catch {
completion(.error(error))
}
}
}
That changes the API of get user, so now when calling get user, a callback will need to be used.
Instead of somethings like this
do {
let user = try manager.getUser(id: "test")
updateUI(user: user)
} catch {
handleError(error)
}
you will need something like this
manager.getUser(id: "test") { [weak self] result in
switch result {
case .value(let user): self?.updateUI(user: user)
case .error(let error): self?.handleError(error)
}
}
Assuming you have somethings like a view controller with a property named manager and methods updateUI() and handleError()