I'm having a hard time figuring out how to create a Swift Combine pipeline that contains a .flatMap which has a reference to self. In order to prevent a retain cycle, this should be a [weak self] reference, but this does not work with a .flatMap.
Here is a simplified example showing my problem:
import Foundation
import Combine
class SomeService {
func someOperation(label: String) -> Future<String, Never> {
Future { promise in
print("Starting work for", label)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
print("Finished work for", label)
promise(.success(label))
}
}
}
}
class SomeDataSource {
let someService = SomeService()
var cancellables = Set<AnyCancellable>()
deinit {
print("Deinit SomeDataSource")
}
func complexOperation() {
// First part 'defined' is inside the complexOperation method:
someService.someOperation(label: "First part")
// Second part is 'defined' in another method (it is shared with other tasks)
.flatMap { _ in self.partOfComplexOperation(label: "Second part") } // <--- This creates a retain cycle
.sink { label in
print("Received value in sink", label)
}
.store(in: &cancellables)
}
func partOfComplexOperation(label: String) -> Future<String, Never> {
someService.someOperation(label: label)
}
}
var someDataSource: SomeDataSource? = SomeDataSource()
someDataSource?.complexOperation()
someDataSource = nil
Output:
Starting work for First part
Finished work for First part
Starting work for Second part
Finished work for Second part
Received value in sink Second part
Deinit SomeDataSource
The problem here is that I want my SomeDataSource to be deinitialised right after starting the "First part" and not even starting the second part. So the output I'm looking for is:
Starting work for First part
Deinit SomeDataSource
Finished work for First part
I'm looking for some kind of combination of .flatMap and .compactMap. Does this exist? If I first .compactMap { [weak self] _ in self } I get the expected result, but maybe there is a better way?
import Foundation
import Combine
class SomeService {
func someOperation(label: String) -> Future<String, Never> {
Future { promise in
print("Starting work for", label)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
print("Finished work for", label)
promise(.success(label))
}
}
}
}
class SomeDataSource {
let someService = SomeService()
var cancellables = Set<AnyCancellable>()
deinit {
print("Deinit SomeDataSource")
}
func complexOperation() {
// First part 'defined' is inside the complexOperation method:
someService.someOperation(label: "First part")
.compactMap { [weak self] _ in self }
// Second part is 'defined' in another method (it is shared with other tasks)
.flatMap { dataSource in dataSource.partOfComplexOperation(label: "Second part") }
.sink { label in
print("Received value in sink", label)
}
.store(in: &cancellables)
}
func partOfComplexOperation(label: String) -> Future<String, Never> {
someService.someOperation(label: label)
}
}
var someDataSource: SomeDataSource? = SomeDataSource()
someDataSource?.complexOperation()
someDataSource = nil
Output:
Starting work for First part
Deinit SomeDataSource
Finished work for First part
The solution here is to not retain self. You don't even want self in the flatMap so why retain it...
let label = someService.someOperation(label: "First part")
.flatMap { [someService] _ in
someService.someOperation(label: label)
}
Of course seeing all this work on someService implies that the service is missing some functionality. Seeing that the result of someOperation is being ignored might also be a red flag.
If you were truly in a situation where you had to use self, then the solution would look like this:
let foo = someOperation()
.flatMap { [weak self] in
self?.otherOperation() ?? Empty(completeImmediately: true)
}
Or you might consider something like:
someOperation()
.compactMap { [weak someService] _ in
someService?.otherOperation()
}
.switchToLatest()
Which will cancel otherOperation() if a new event comes through from someOperation().
Related
I implemented a service class with a function that returns a publisher when some data is loaded:
class Service {
let fileURL: URL // Set somewhere else in the program
func loadModels() -> AnyPublisher<[MyModelClass], Error> {
URLSession.shared.dataTaskPublisher(for: fileURL)
.map( { $0.data } )
.decode(type: [MyModelClass].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
This function is used in my view model class like this:
class ViewModel: ObservableObject {
#Published var models: [MyModelClass]?
var cancellables = Set<AnyCancellable>()
let service: Service
init(service: Service) {
self.service = service
loadCityData()
}
func loadModels() {
service.loadModels()
.sink { _ in
} receiveValue: { [weak self] models in
self?.models = models
}
.store(in: &cancellables)
}
}
I find the view model difficult to unit-test because I don't have the publisher returned from the service available directly in my unit test class, but I have the #Published property instead. So I tried to implement a test like this one:
let expectation = expectation(description: "loadModels")
viewModel.$models
.receive(on: RunLoop.main)
.sink(receiveCompletion: { _ in
finishLoading.fulfill()
}, receiveValue: { _ in
})
.store(in: &cancellables) // class-scoped property
viewModel.loadModels()
wait(for: [expectation], timeout: 10)
The problem is that the receiveComplection callback is never called. If I had the publisher available (the one returned from the Service object), the same code applied to the publisher would run successfully and fulfill the expectation. Instead, the complection is not being called but the receiveValue is being called multiple times. Why?
First, instead of passing the entire service to the view model, just pass in the Publisher itself. In the test, you can pass in a synchronous publisher which makes testing much easier.
final class ExampleTests: XCTestCase {
func test() {
let input = [MyModelClass()]
let modelLoader = Just(input).setFailureType(to: Error.self).eraseToAnyPublisher()
let viewModel = ViewModel(modelLoader: modelLoader)
let cancellable = viewModel.$models
.dropFirst(1)
.sink(receiveValue: { output in
XCTAssertEqual(input, output)
})
viewModel.loadModels()
}
}
class ViewModel: ObservableObject {
#Published var models: [MyModelClass]?
var cancellables = Set<AnyCancellable>()
let modelLoader: AnyPublisher<[MyModelClass], Error>
init(modelLoader: AnyPublisher<[MyModelClass], Error>) {
self.modelLoader = modelLoader
}
func loadModels() {
modelLoader
.sink { _ in
} receiveValue: { [weak self] models in
self?.models = models
}
.store(in: &cancellables)
}
}
Notice that there is no need to setup an expectation and wait for it. This makes for a much faster test.
Even simpler would be to just examine the models property directly:
final class ExampleTests: XCTestCase {
func test() {
let input = [MyModelClass()]
let modelLoader = Just(input).setFailureType(to: Error.self).eraseToAnyPublisher()
let viewModel = ViewModel(modelLoader: modelLoader)
viewModel.loadModels()
XCTAssertEqual(viewModel.models, input)
}
}
For all of these though, what exactly do you think you are testing here? There are no transformations and no logic in this code. You aren't testing to ensure the ViewModel calls into the Service, because in order to do this test at all, you have to mock out the Service. So in reality, the only thing you are doing is testing to see if the test itself mocked out the Service correctly. But what's the point in that? Who cares if the test was set up correctly if it doesn't test production code?
You could use a combination of interface and dependency injection to allow testing.
First you define an interface for service:
protocol ServiceInterface {
func loadModels() -> AnyPublisher<[MyModelClass], Error>
}
Next you make Service conform to this new protocol:
class Service: ServiceInterface {
// ...
}
Now you can inject Service into your ViewModel using the interface defined above:
class ViewModel: ObservableObject {
//...
let service: ServiceInterface
init(service: ServiceInterface = Service()) {
self.service = service
loadModels()
}
//...
}
This means you are able to inject any entity conforming to ServiceInterface into the ViewModel, so let's define one in the test target:
struct MockService: ServiceInterface {
let loadModelsResult: Result<[MyModelClass], Error>
func loadModels() -> AnyPublisher<[MyModelClass], Error> {
loadModelsResult.publisher.eraseToAnyPublisher()
}
}
Lastly let's inject MockService into ViewModel for testing purposes:
func testExample() {
let expectedModels = [MyModelClass()]
let subject = ViewModel(service: MockService(loadModelsResult: .success(expectedModels)))
let expectation = expectation(description: "expect models to get loaded")
subject
.$models
.sink(
receiveCompletion: { _ in },
receiveValue: { actualModels in
// or any other test that is meaningful in your context
if actualModels == expectedModels {
expectation.fulfill()
}
}
)
.store(in: &cancellables)
subject.loadModels()
waitForExpectations(timeout: 0.5)
}
I am new to RxSwift and RxCocoa
I need to any advice for learning
After result of Checking Id Validation, expect no word in label
But it is updating label and no entering in break point at bind function
What’s problem my code…?
var disposeBag: DisposeBag = DisposeBag()
let viewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
let input: Signal<String> = userIDTextField.rx.text.orEmpty
.asSignal(onErrorSignalWith: .empty())
let output: Driver<String> = viewModel.bind(input)
disposeBag.insert(
output.drive(userIDLabel.rx.text)
)
}
struct ViewModel {
func checkUserIDFromDB(id: String) -> Signal<Bool> {
return .just(false).asSignal()
}
func bind(_ input: Signal<String>) -> Driver<String> {
let validState = input
.map { _ in self.checkUserIDFromDB(id:)}
.withLatestFrom(input)
return validState.asDriver(onErrorDriveWith: .empty())
}
}
This line: .map { _ in self.checkUserIDFromDB(id:)} produces a Signal<(String) -> Signal<Bool>> which is likely not what you wanted.
I'm going to assume that the goal here is to pass the entered string to the network request and wait for it to emit. If it emits true then emit the string to the label, otherwise do nothing...
Further, let's simplify things by using the Observable type instead of Signals and Drivers:
final class ViewController: UIViewController {
let userIDTextField = UITextField()
let userIDLabel = UILabel()
let disposeBag = DisposeBag() // this should be a `let` not a `var`
let viewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
let input = userIDTextField.rx.text.orEmpty
let output = viewModel.bind(input.asObservable())
disposeBag.insert(
output.bind(to: userIDLabel.rx.text)
)
}
}
struct ViewModel {
func checkUserIDFromDB(id: String) -> Observable<Bool> { .just(false) }
func bind(_ input: Observable<String>) -> Observable<String> {
input.flatMapLatest { id in // Note this should be `flatMapLatest` not `map`
Observable.zip( // zip up the text with its response
Observable.just(id),
self.checkUserIDFromDB(id: id) // you weren't actually making the network call. This makes it.
.catchAndReturn(false) // if the call fails, emit `false`.
)
}
.compactMap { $0.1 ? $0.0 : nil } // if the response is true, emit the text, else nothing
}
}
The biggest concern I have with this code is what happens if the user continues to type. This will fire after every character the user enters which could be a lot of network requests, the flatMapLatest will cancel ongoing requests that are no longer needed, but still... Consider putting a debounce in the stream to reduce the number of requests.
Learn more about the various versions of flatMap from this article.
Edit
In response to your comment. In my opinion, a ViewModel should not be dependent on RxCocoa, only RxSwift. However, if you feel you must use Driver, then something like this would be appropriate:
func bind(_ input: ControlProperty<String>) -> Driver<String> {
input.asDriver()
.flatMapLatest { id in
Driver.zip(
Driver.just(id),
self.checkUserIDFromDB(id: id)
.asDriver(onErrorJustReturn: false)
)
}
.compactMap { $0.1 ? $0.0 : nil }
}
Using Signal doesn't make much sense in this context.
I recently started to study the Combine and ran into a certain problem.
First, I will describe what I am doing.
I trying to use Clean Architecture
Here you can see my Repository
protocol Repository {
func test()
}
class MockRepository: Repository {
func test() {
sleep(3)
}
}
Then I created UseCase
class UseCaseBase<TInput, TOutput> {
var task: TOutput? { return nil }
var repository: Repository
init(_ repository: Repository) {
self.repository = repository
}
func execute(with payload: TInput) -> AnyPublisher<TOutput, Never> {
return AnyPublisher(Future<TOutput, Never> { promise in
promise(.success(self.task!))
})
.eraseToAnyPublisher()
}
}
class MockUseCase: UseCaseBase<String, Int> {
override var task: Int? {
repository.test()
return 1
}
}
And then in a init block ContentView I did something like that
init() {
let useCase = MockUseCase(MockRepository())
var cancellables = Set<AnyCancellable>()
useCase.execute(with: "String")
.sink(receiveValue: { value in
print(value)
})
.store(in: &cancellables)
print("Started")
}
At first, I want to get
"Started"
and then after sleep(3)
value "1"
Now I get
"1" and then "Started"
Your sleep(3) call runs on the main thread, which means that it blocks any other operations, including the code that prints the "Started" text.
I won't be rambling about how bad it is to block the main thread, this is well known information, but this is the reason you see the behaviour you asked about.
I don't see any thread switching code in your question, so if you wish to achieve some kind of asynchronicity, then you can either go with Rob's solution of using dispatch(after:), or do the locomotion (the sleep) on another thread:
func execute(with payload: TInput) -> AnyPublisher<TOutput, Never> {
return AnyPublisher(Future<TOutput, Never> { promise in
DispatchQueue.global().async {
promise(.success(self.task!))
}
})
.eraseToAnyPublisher()
}
The first thing I'll mention is that you need to hold a reference to cancellables or your publisher will automatically be cancelled when you add async processing to the chain. Move it out of your init method and into a property.
You can also get rid of the sleep and simply chain to a Delay publisher on a queue of your choice. I chose main.
struct SomeThing {
var cancellables = Set<AnyCancellable>()
init() {
MockUseCase(MockRepository())
.execute(with: "String")
.delay(for: 3.0, scheduler: DispatchQueue.main)
.sink(receiveValue: { print($0) } )
.store(in: &cancellables)
print("Started")
}
}
class MockRepository: Repository {
func test() {
// sleep(3)
}
}
Another option is to get rid of the delay, get rid of the sleep and fulfill your promise asynchronously:
struct SomeThing {
var cancellables = Set<AnyCancellable>()
init() {
MockUseCase(MockRepository())
.execute(with: "String")
.sink(receiveValue: { print($0) } )
.store(in: &cancellables)
print("Started")
}
}
class MockRepository: Repository {
func test() {
// sleep(3)
}
}
func execute(with payload: TInput) -> AnyPublisher<TOutput, Never> {
return Future<TOutput, Never> { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
promise(.success(self.task!))
}
}
.eraseToAnyPublisher()
}
Is there a way to get the value of a PassthroughSubject publisher in a Unit test?
I want to test that a function returns success and to test this one I want to see when the publisher value is .loaded, then is success.
class HomeViewModel: ObservableObject {
var homeState = PassthroughSubject<StatePublisher, Never>()
func load(item: HomeModel) {
self.homeState.send(.loading)
self.dataSource.load(item: item) { result in
switch result {
case .success:
self.homeState.send(.loaded)
case let .failure(error):
self.homeState.send(.error(message: error.localizedDescription))
}
}
}
}
class HomeViewModelTests: XCTestCase {
var sut: ViewModel!
var subscriptions = Set<AnyCancellable>()
override func setUpWithError() throws {
sut = ViewModel()
}
override func tearDownWithError() throws {
sut = nil
subscriptions = []
}
func testUpdateHomeSuccess() {
let expected = StatePublisher.loaded
var result = StatePublisher.loading
sut.load(item: HomeModel.fixture())
sut.homeState
.sink(receiveValue: { state in
result = state
})
.store(in: &subscriptions)
XCTAssert(
result == expected,
"Home expected to be \(expected) but was \(result)"
)
}
}
I tried a test like this, but sink is never called.
Presumably, dataSource.load operates asynchronously. That means you need to use an XCTestExpectation in your test case. Also, because you're using a PassthroughSubject rather than a CurrentValueSubject, you should create your subscription before you start the load, to be sure you can't miss any published values.
let ex = XCTestExpectation()
sut.homeState
.sink {
if case .loaded = $0 {
ex.fulfill()
}
}
.store(in: &subscriptions)
wait(for: [ex], timeout: 10)
// Test fails if it doesn't call `ex.fulfill()` within 10 seconds.
Read Apple's guide to Testing Asynchronous Operations with Expectations.
I changed my PassthroughSubject to CurrentValueSubject as then the state of the value is present also when executing the XCTestCase.
So this test was always failing: (the PassthroughSubject is declared in viewModel class which is tested)
var someValue: PassthroughSubject<Bool, Never> = .init()
func someTest() throws {
let exp = expectation(description: "Value changed")
viewModel.functionChangingTheValue()
viewModel.someValue.sink { result in
XCTAssertTrue(result)
exp.fulfill()
}
.store(in: &cancellables)
wait(for: [exp], timeout: 0.1)
}
But this one is now successful
var someValue: CurrentValueSubject<Bool, Never> = .init(false)
func someTest() throws {
let exp = expectation(description: "Value changed")
viewModel.functionChangingTheValue()
viewModel.someValue.sink { result in
XCTAssertTrue(result)
exp.fulfill()
}
.store(in: &cancellables)
wait(for: [exp], timeout: 0.1)
}
I am using the Redux pattern for building a messaging application. Everything works fine so far but then I notice a memory leak in some parts of the app that I'm unable to solve. My view controller that binds to messages publisher. Deinit won't get called when the view controller is dismissed.
let messages = {
store.$state
.map { $0.chatState.messagesByChannel[self.channelId] }
.removeDuplicates()
.eraseToAnyPublisher()
}()
messages.combineLatest(Just("Hello world"))
.sink { [weak self] (messages, state) in
}
.store(in: &cancellableSet)
When I changed from referencing a dictionary object to another object in the chat state deinit gets called
let chatRoomDetailResponse = {
store.$state
.map { $0.chatState.getChatRoomDetailResponse }
.removeDuplicates()
.eraseToAnyPublisher()
}()
chatRoomDetailResponse.combineLatest(Just("Hello world"))
.sink { [weak self] (messages, state) in
}
.store(in: &cancellableSet)
This is a small snapshot of my store:
final public class Store<State: FluxState>: ObservableObject {
#Published public var state: State
private var dispatchFunction: DispatchFunction!
private let reducer: Reducer<State>
and my ChatState:
public struct ChatState: FluxState {
public typealias ChannelID = String
public var messagesByChannel: [ChannelID: [Message]] = [:]
public var getChatRoomDetailResponse: NetworkResponse<ChatChannel>? = nil
}
$0.chatState.messagesByChannel[self.channelId] is capturing self strongly, for the sake of being able to access its most-up-to-date channelId value.
Either catpure self weakly:
.map { [weak self] in
guard let strongSelf = self else { return ??? }
$0.chatState.messagesByChannel[strongSelf.channelId]
}
Or if channelId doesn't change, you can use a capture list to capture it by value:
.map { [channelId] in $0.chatState.messagesByChannel[channelId] }