Swift Combine sink called once at setup? - ios

I am setting up a sink like so:
name.publisher
.removeDuplicates()
.receive(on: RunLoop.main)
.sink { [weak self] config in
guard let self = self else { return }
// this closure gets called right away at setup even though the #Published property `name` was already setup and did not change
}.store(in: &subscribers)
The property is declared like so in an observable object:
#Published var name:String = ""
So, I'm obviously missing something here. Why is sink called once at setup even though name did not change? I can avoid this behavior by using the dropFirst() operator but, I'd like to understand why the closure is always called once immediately after setup?
Why is that?

Here's a playground that uses debugPrint to show you what you get from name.publisher:
import UIKit
import Combine
//Holds the score
class ViewModel : ObservableObject {
#Published var name = "0"
}
let viewModel = ViewModel()
debugPrint(viewModel.name.publisher)
What you get is
Combine.Publishers.Sequence<Swift.String, Swift.Never>(sequence: "0")
So you get a sequence publisher that has a single item, "0". It will publish that value once and the sequence will end. Each time a subscriber attaches to that sequence it will get all the items in the sequence (there's only one) and the end.
This is probably not what you want.
Instead I think you want to use $name to access the published property:
import UIKit
import Combine
//Holds the score
class ViewModel : ObservableObject {
#Published var name = "0"
}
let viewModel = ViewModel()
let subscription = viewModel.$name.sink { print($0) }
viewModel.name = "Alex"
When you subscribe to the published property you will still get a posted event that is the current value of the property. However, by using $name you are attaching to a stream that will send you the current value on subscription AND each subsequent value.

Related

Custom Combine Publisher for a single database listener

I have an iOS app with a custom database, To retrieve data from my database I setup a listener like this:
var listener: DatabaseListener?
self.listener = db.items.addListener { items in
}
// later when I don't need the listener any more:
self.listener?.cancel()
This listener gives the items as soon as I set it up and notifies me whenever my data is updated, It also stays alive until I manually cancel it. I also store a cache of the retrieved items in UserDefaults to speed things up (See it in action in the example bellow).
Now I'm trying to start using Combine to retrieve my items, I want to setup the database listener as soon as a new subscription is created (for example when sink or assign are called) and cancel it when there's no more subscriptions left.
So here's what I came up with:
class ItemsSubscription: Subscription {
private var subscriber: (any Subscriber<[Item], Never>)?
private var listener: DatabaseListener?
private var items: [Item] = UserDefaults.standard.cacheItems
init(subscriber: any Subscriber<[Item], Never>) {
self.subscriber = subscriber
}
func request(_ demand: Subscribers.Demand) {
let _ = subscriber?.receive(items)
self.listener = db.items.addListener {
UserDefaults.standard.cacheItems = $0
self.items = $0
let _ = self.subscriber?.receive($0)
}
}
func cancel() {
self.listener?.cancel()
self.listener = nil
self.subscriber = nil
}
}
struct ItemsPublisher: Publisher {
typealias Output = [Item]
typealias Failure = Never
func receive<S>(subscriber: S) where S: Subscriber, S.Input == [Item], S.Failure == Never {
let subscription = ItemsSubscription(subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
Then I'm using ItemsPublisher like this:
private var cancellables: Set<AnyCancellable> = []
ItemsPublisher()
.sink { items in
}
.store(&cancellables)
Currently this method is working but it's creating a new database listener (which is an expensive resource) for every ItemsPublisher I create. Instead I want to maintain a single database listener while I have a least 1 subscriber and I want any following subscriber to receive the latest items from the same subscription.
I considered creating a single ItemsPublisher instance and using it throughout the app, but later subscribers didn't receive any data at all.
I also considered using CurrentValueSubject (or a #Published property) to store the items but I couldn't figure out when to setup database listener or when to cancel it for that matter.
Any help or advice would be appreciated.
Instead I want to maintain a single database listener while I have a least 1 subscriber and I want any following subscriber to receive the latest items from the same subscription.
That's exactly what share() is for. View the documentation for more information.
You might also want to consider using multicast with a CurrentValueSubject depending on the situation.

Swift Combine: direct output of one Publisher to the input of another

Consider the following synthetic scenario:
import Combine
let publisher1 = PassthroughSubject<Int, Never>().eraseToAnyPublisher()
let publisher2 = PassthroughSubject<Int, Never>()
publisher1.sink { value in
publisher2.send(value)
}
We have 2 publishers, I'd like to propagate any value of the publisher1 to the publisher2. The code I've shown does the job, but I'm interested whether there is a cleaner, declarative approach to this.
Note: both publisher1 and publisher2 are of the same type.
Details on the problem
publisher2 is a part of the API that is exposed by the "core" class. The "core" class has a "has a" relationship to a "child" class which in turn has a publisher1 as it's API.
Over the lifetime of the "core" class, the "child" class can be allocated and deallocated multiple times. This should be transparent to the subscribers of the "core" class which won't need to subscribe to the publisher2.
Code:
import UIKit
import Combine
class ChildClass {
let publisher1 = PassthroughSubject<Int, Never>().eraseToAnyPublisher()
}
class CoreClass {
let publisher2 = PassthroughSubject<Int, Never>()
private var childClass: ChildClass?
init() {
allocateChildClass()
}
func allocateChildClass() {
let child = ChildClass()
childClass = child
// Any way to simplify this?
child.publisher1.sink { value in
publisher2.send(value)
}
}
func deallocateChildClass() {
childClass = nil
}
}
class Client {
let core = CoreClass()
init() {
// Doesn't care about allocating or deallocating of the ChildClass
core.publisher2.sink { value in
print(value)
}
}
}
Trying to subscribe one publisher to another doesn't work:
publisher2
.subscribe(publisher1)
No exact matches in call to instance method 'subscribe'
It is a delightful feature of PassthroughSubject that it is both a publisher and an operator: a Subject can be chained directly to a pipeline.
So just say
publisher1.subscribe(publisher2)
and you're all set.

XCTest testing asyncronous Combine #Publishers [duplicate]

This question already has answers here:
How To UnitTest Combine Cancellables?
(2 answers)
Closed 1 year ago.
I'm working on an iOS app (utilizing Swift, XCTest, and Combine) trying to test a function within my view model, which is calling and setting a sink on a publisher. I'd like to test the view model, not the publisher itself. I really don't want to use DispatchQueue.asyncAfter( because theoretically I don't know how long the publisher will take to respond. For instance, how would I test XCTAssertFalse(viewModel.isLoading)
class ViewModel: ObservableObject {
#Published var isLoading: Bool = false
#Published var didError: Bool = false
var dataService: DataServiceProtocol
init(dataService: DataServiceProtocol) {
self.dataService = dataService
}
func getSomeData() { // <=== This is what I'm hoping to test
isLoading = true
dataService.getSomeData() //<=== This is the Publisher
.sink { (completion) in
switch completion {
case .failure(_):
DispatchQueue.main.async {
self.didError = true
}
case .finished:
print("finished")
}
DispatchQueue.main.async {
self.isLoading = false
}
} receiveValue: { (data) in
print("Ok here is the data", data)
}
}
}
I'd like to write a test that might look like:
func testGetSomeDataDidLoad() {
// this will test whether or not getSomeData
// loaded properly
let mockDataService: DataServiceProtocol = MockDataService
let viewModel = ViewModel(dataService: mockDataService)
viewModel.getSomeData()
// ===== THIS IS THE PROBLEM...how do we know to wait for getSomeData? ======
// It isn't a publisher...so we can't listen to it per se... is there a better way to solve this?
XCTAssertFalse(viewModel.isLoading)
XCTAssertFalse(viewModel.didError)
}
Really hoping to refactor our current tests so we don't utilize a DispatchQueue.asyncAfter(
Yeah, everybody's saying, MVVM increases testability. Which is hugely true, and thus a recommended pattern. But, how you test View Models is shown only very rarely in tutorials. So, how can we test this thing?
The basic idea testing a view model is using a mock which can do the following:
The mock must record changes in its output (which is the published properties)
Record a change of the output
Apply an assertion function to the output
Possibly record more changes
In order to work better with the following tests, refactor your ViewModel slightly, so it gets a single value representing your view state, using a struct:
final class MyViewModel {
struct ViewState {
var isLoading: Bool = false
var didError: Bool = false
}
#Published private(set) var viewState: ViewState = .init()
...
}
Then, define a Mock for your view. You might try something like this, which is a pretty naive implementation:
The mock view also gets a list of assertion functions which test your view state in order.
class MockView {
var viewModel: MyViewModel
var cancellable = Set<AnyCancellable>()
typealias AssertFunc = (MyViewModel.ViewState) -> Void
let asserts: ArraySlice<AssertFunc>
private var next: AssertFunc? = nil
init(viewModel: MyViewModel, asserts: [AssertFunc]) {
self.viewModel = viewModel
self.asserts = ArraySlice(asserts)
self.next = asserts.first
viewModel.$viewState
.sink { newViewState in
self.next?(newViewState)
self.next = self.asserts.dropFirst().first
}
}
}
You may setup the mock like this:
let mockView = MockView(
viewModel: viewModel,
asserts: [
{ state in
XCTAssertEqual(state.isLoading, false)
XCTAssertEqual(state.didError, false)
},
{ state in
XCTAssertEqual(state.isLoading, true)
...
},
...
])
You can also use XCT expectation in the assert functions.
Then, in your test you create the view model, your mock data service and the configured mockView.
let mockDataService: DataServiceProtocol = MockDataService
let viewModel = ViewModel(dataService: mockDataService)
let mockView = MockView(
viewModel: viewModel,
asserts: [
{ state in
XCTAssertEqual(state.isLoading, false)
XCTAssertEqual(state.didError, false)
},
...
{ state in
XCTAssertEqual(state.isLoading, false)
XCTAssertEqual(state.didError, false)
expectFinished.fulfill()
},
...
])
viewModel.getSomeData()
// wait for the expectations
Caution: I didn't compile or run the code.
You may also take a look at Entwine

How to stop storing AnyCancellable after Swift Combine Sink has received at least one value?

I have a sink that needs to be canceled as soon as I receive the first value. I don't care about future values, just the first one published. Because without storing the AnyCancelable created by the sink, the garbage collector will delete the sink, I must store it. At the same time, I must also clean it up after the sink has completed, otherwise, I will have a memory leak.
I built one using a UUID → AnyCancelable map, but I am worried that this is more complex than it should be; is there another way of doing this? What's recommended by Combine?
#Published var locationState: (location: CLLocation?, error: Error?)?
var requestLocationSinks: [String: AnyCancellable] = [:]
// 1. Generate ID to uniquely identify the current sink.
let sinkID = UUID().uuidString
// 2. Start the sink and store it in our ID → AnyCancellable dictionary.
requestLocationSinks[sinkID] = $locationState.sink { locationState in
if let locationState = locationState {
invokeCallbackWithLocationState(locationState)
}
// 3. Remove the stored AnyCancellable as soon as we received our first value!
self.requestLocationSinks.removeValue(forKey: sinkID)
}
If you just need to keep it alive until the sink is called once, you can just create a temporary variable
var cancellable: AnyCancellable?
cancellable = $locationState.sink { locationState in
if let locationState = locationState {
invokeCallbackWithLocationState(locationState)
}
cancellable = nil
}
This will retain the AnyCancellable long enough (because the closure retains the reference)
if you are only interested in the first element of the upstream you can use either the operator .first() or the operator .prefix(1). They are equal. With .prefix(n) you can define the number of passed upstream elements.
myPublisher
.first()
.sink( ... )
or
myPublisher
.prefix(1 or n)
.sink( ... )
Please note, that the two operators above, cause an .finished completion. This means that the subscription will be deleted after the operation.
publisher is triggered -> receiveValue -> receiveCompletion (finished) -> subscription is deleted
If you work with AnyCancellable and want to delete your subscription, you should use the .cancel() function of the AnyCancellable .
var cancellable: AnyCancellable?
cancellable = myPublisher
.sink(receiveValue: { value in
// Your logic.
cancellable.cancel()
})
I suppose that in your case the operator .first() is exactly what you need.

Property Wrapper for CurrentValueSubject - memory management

I would like to create a property wrapper for CurrentValueSubject. I have done this like that:
#propertyWrapper
public class CurrentValue<Value> {
public var wrappedValue: Value {
get { projectedValue.value }
set { projectedValue.value = newValue }
}
public var projectedValue: CurrentValueSubject<Value, Never>
public init(wrappedValue: Value) {
self.projectedValue = CurrentValueSubject(wrappedValue)
}
}
This works but there is a little thing I would like to change with it - use struct instead of class. The problem with using struct for this is that sometimes I could get Simultaneous accesses error. And I know why, this happens when in sink from this publisher I would try to read the value from wrapped value. So for example with code like this:
#CurrentValue
let test = 1
$test.sink { _ in
print(self.test)
}
And I more or less know why - because when projectedValue executes its observation, wrapped value is still in process of setting its value. In class this is ok, because it would just change the value, but with struct it actually modifies the struct itself, so Im trying to write and read from it at the same time.
My question is - is there some clever way to overcome this, while still using struct? I don't want to dispatch async.
Also I know that #Projected works similarly to this propertyWrapper, but there is a big difference - Projected executes on willSet, while CurrentValueSubject on didSet. And Projected has the same issue anyway.
I know that I can read the value inside the closure, but sometimes Im using this with various function calls, that might eventually use self.test instead.
Try my implementation
#propertyWrapper
class PublishedSubject<T> {
var wrappedValue: T {
didSet {
subject.send(wrappedValue)
}
}
var projectedValue: some Publisher<T, Never> {
subject
}
private lazy var subject = CurrentValueSubject<T, Never>(wrappedValue)
init(wrappedValue: T) {
self.wrappedValue = wrappedValue
}
}

Resources