Not receiving inputs when using `.receive(on: DispatchQueue.main)` - ios

I’m trying to change to the main thread in the downstream with .receive(on: DispatchQueue.main) but then I don’t receive inputs when using either .subscribe(:) or .sink(receiveValue:). If I don’t change threads I do receive the proper inputs.
Publisher
extension URLSessionWebSocketTask {
struct ReceivePublisher: Publisher {
typealias Output = Message
typealias Failure = Error
let task: URLSessionWebSocketTask
func receive<S>(subscriber: S) where S: Subscriber, Output == S.Input, Failure == S.Failure {
task.receive { result in
switch result {
case .success(let message): _ = subscriber.receive(message)
case .failure(let error): subscriber.receive(completion: .failure(error))
}
}
}
}
}
extension URLSessionWebSocketTask {
func receivePublisher() -> ReceivePublisher {
ReceivePublisher(task: self)
}
}
Subscriber
extension ViewModel: Subscriber {
typealias Input = URLSessionWebSocketTask.Message
typealias Failure = Error
func receive(subscription: Subscription) {}
func receive(_ input: URLSessionWebSocketTask.Message) -> Subscribers.Demand {
// Handle input here.
// When using `.receive(on:)` this method is not called when should be.
return .unlimited
}
func receive(completion: Subscribers.Completion<Error>) {}
}
Subscribe
socketTask.receivePublisher()
.receive(on: DispatchQueue.main)
.subscribe(viewModel)
socketTask.resume()

The AnyCancellable returned by subscribe<S>(_ subject: S) -> AnyCancellable will call cancel() when it has been deinitialized. Therefore if you don't save it it will be deinitialized when the calling block goes out of scope.
Out of the videos and tutorials I have seen from WWDC, how to work with this was never addressed. What I've seen is that people are drifting towards RxSwift's DisposeBag solution.
Update Beta 4:
Combine now comes with a method on AnyCancellable called: store(in:) that does pretty much what my old solution does. You can just store the AnyCancellables in a set of AnyCancellable:
var cancellables = Set<AnyCancellable>()
...
override func viewDidLoad() {
super.viewDidLoad()
...
socketTask.receivePublisher()
.receive(on: DispatchQueue.main)
.subscribe(viewModel)
.store(in: &cancellables)
}
This way the array (and all AnyCancellables) will be deinitialized when the containing class is deinitialized.
Outdated:
If you want a solution for all Cancellables that can be used in a way that flows better you could extend Cancellable as such:
extension Cancellable {
func cancel(with cancellables: inout [AnyCancellable]) {
if let cancellable = self as? AnyCancellable {
cancellables.append(cancellable)
} else {
cancellables.append(AnyCancellable(self))
}
}
}

Related

How to use a publisher in base class and observe changes in two different subclasses?

I have a base class where I'm making an api call and changing the state of the api like so
class ProfileBaseViewModel {
#Published private(set) var apiState: APIState = .initial
private(set) var settingsData: SettingModel?
var cancellables: Set<AnyCancellable> = Set<AnyCancellable>()
func getSettingsData() {
apiState = .inProgress
interactor.getSettingsData()
.sink(receiveCompletion: { [weak self] error in
if case .failure(_) = error {
self?.apiState = .completed(didSucceed: false)
}
}, receiveValue: { [weak self] response in
guard let self: ProfileBaseViewModel = self else { return }
self.settingsData = response
self.apiState = .completed(didSucceed: true)
})
.store(in: &self.cancellables)
}
}
This base class has two child view model's like so. Notice how getSettingsData is only being called in ProfileViewModel, this is because SettingsVC is pushed on top of ProfileVC and expectation is that ProfileVC/VM will make the api call and only the api state will be observed in SettingsVC/VM
final class ProfileViewModel: ProfileBaseViewModel {
init() {
getSettingsData()
}
}
final class SettingViewModel: ProfileBaseViewModel {
}
These two view models are attached to two view controllers like so
extension ProfileViewController {
func observeViewModel() {
self.viewModel.$apiState
.receive(on: DispatchQueue.main)
.sink { [weak self] state in
self?.handleAPIState(state: state)
}
.store(in: &super.cancellables)
}
private func handleAPIState(state: APIState) {
switch state {
case .initial:
break
case .inProgress:
///show loader
case .completed(let didSucceed):
///reload table
}
}
}
extension SettingsViewController {
func observeViewModel() {
self.viewModel.$apiState
.receive(on: DispatchQueue.main)
.sink { [weak self] state in
self?.handleAPIState(state: state)
}
.store(in: &super.cancellables)
}
private func handleAPIState(state: APIState) {
switch state {
case .initial:
break
case .inProgress:
///show loader
case .completed(let didSucceed):
///reload table
}
}
}
This doesn't work as expected, handleAPIState in SettingsVC is only called for initial state and not for inProgress or completed, it works as expected for ProfileVC. Any help is appreciated.

Combine assign(to: on:) another publisher

Here is a simple "Download" class to illustrate what I want to do.
class Download {
public var progress: CurrentValueSubject<Double, Never> = CurrentValueSubject<Double, Never>(0)
var subscriptions: Set<AnyCancellable> = []
func start(task: URLSessionTask) {
task.resume()
task.progress.publisher(for: \.fractionCompleted).sink { [weak self] (newProgress) in
self?.progress.send(newProgress)
}.store(in: &subscriptions)
}
}
I would like to be able to "re-publish" the progress property observer publisher to my current value subject. As you can see I currently subscribe using the .sink function and then just call the CurrentValueSubject publisher directly.
I would like to be able to use something like the .assign(to:, on:) operator like this.
task.progress.publisher(for: \.fractionCompleted).assign(to: \.progress, on: self)
However, that will not work nor will the .assign(to:) operator that seems to be reserved for "re-publishing" on a SwiftUI #Published property. Why is Combine not living up to it's name here?
Because CurrentValueSubject is a class, you can use it as the object argument of assign(to:on:). This way, there is no memory leak:
class Download {
public let progress: CurrentValueSubject<Double, Never> = .init(0)
var subscriptions: [AnyCancellable] = []
func start(task: URLSessionTask) {
task.resume()
task.progress
.publisher(for: \.fractionCompleted)
.assign(to: \.value, on: progress)
.store(in: &subscriptions)
}
}
You need to assign to the value of the subject, not the subject itself. It is worth noting though that assign(to: ..., on: self) leads to a memory leak 🤷🏻‍♀️.
func start(task: URLSessionTask) {
task.resume()
task.progress
.publisher(for: \.fractionCompleted)
.assign(to: \.progress.value, on: self)
.store(in: &subscriptions)
}

How to wrap the delegate pattern with a one-shot publisher?

Normally we can bridge our async code and Combine by wrapping our async code in a single-shot publisher using a Future:
func readEmail() -> AnyPublisher<[String], Error> {
Future { promise in
self.emailManager.readEmail() { result, error in
if let error = error {
promise(.failure(error))
} else {
promise(.success(result))
}
}
}.eraseToAnyPublisher()
}
On the other hand, if we're wrapping the delegate pattern (instead of an async callback), it's recommended to use a PassthroughSubject, since the methods could be fired multiple times:
final class LocationHeadingProxy: NSObject, CLLocationManagerDelegate {
private let headingPublisher: PassthroughSubject<CLHeading, Error>
override init() {
headingPublisher = PassthroughSubject<CLHeading, Error>()
// ...
}
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
headingPublisher.send(newHeading)
}
}
However, I'm trying to create a one-shot publisher which wraps an existing Delegate pattern. The reason is that I'm firing off a method like connect() and I expect either a success or failure to happen immediately. I do not want future updates to affect the pipeline.
For example, imagine I'm using WKExtendedRuntimeSession and wrapped the .start() method in startSession() below. If I have this wrapped successfully, I should be able to use it like so:
manager.startSession()
.sink(
receiveCompletion: { result in
if result.isError {
showFailureToStartScreen()
}
},
receiveValue: { value in
showStartedSessionScreen()
})
.store(in: &cancellables)
The reason a one-shot publisher is useful is because we expect one of the following two methods to be called soon after calling the method:
Success: extendedRuntimeSessionDidStart(_:)
Fail: extendedRuntimeSession(_:didInvalidateWith:error:)
Furthermore, when the session is halted (or we terminate it ourselves), we don't want side effects such as showFailureToStartScreen() to randomly happen. We want them to be handled explicitly elsewhere in the code. Therefore, having a one-shot pipeline is beneficial here so we can guarantee that sink is only called once.
I realize that one way to do this is to use a Future, store a reference to the Promise, and call the promise at a later time, but this seems hacky at best:
class Manager: NSObject, WKExtendedRuntimeSessionDelegate {
var session: WKExtendedRuntimeSession?
var tempPromise: Future<Void, Error>.Promise?
func startSession() -> AnyPublisher<Void, Error> {
session = WKExtendedRuntimeSession()
session?.delegate = self
return Future { promise in
tempPromise = promise
session?.start()
}.eraseToAnyPublisher()
}
func extendedRuntimeSessionDidStart(_ extendedRuntimeSession: WKExtendedRuntimeSession) {
tempPromise?(.success(()))
tempPromise = nil
}
func extendedRuntimeSession(_ extendedRuntimeSession: WKExtendedRuntimeSession, didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason, error: Error?) {
if let error = error {
tempPromise?(.failure(error))
}
tempPromise = nil
}
}
Is this really the most elegant way to work with delegates + one-shot publishers, or is there a more elegant way to do this in Combine?
For reference, PromiseKit also has a similar API to Future.init. Namely, Promise.init(resolver:). However, PromiseKit also seems to natively support the functionality I describe above with their pending() function (example):
func startSession() -> Promise {
let (promise, resolver) = Promise.pending()
tempPromiseResolver = resolver
session = WKExtendedRuntimeSession()
session?.delegate = self
session?.start()
return promise
}
You can ensure a one-shot publisher with .first() operator:
let subject = PassthroughSubject<Int, Never>()
let publisher = subject.first()
let c = publisher.sink(receiveCompletion: {
print($0)
}, receiveValue: {
print($0)
})
subject.send(1)
subject.send(2)
The output would be:
1
finished

Prevent disposal of PublishSubject (RxSwift)

I'm struggling with specific use-case incorporating RxSwift's PublishSubject.
For sake of simplicity unimportant details were omitted.
There is a MVVM setup. In VC I have a UIButton, on tap of which a network call should dispatch. In ViewModel I have a buttonDidTapSubject: PublishSubject<Void>.
class ViewModel {
let disposeBag = DisposeBag()
let buttonDidTapSubject = PublishSubject<Void>()
let service: Service
typealias Credentials = (String, String)
var credentials: Observable<Credentials> {
return Observable.just(("testEmail", "testPassword"))
}
init(_ service: Service) {
self.service = service
buttonDidTapSubject
.withLatestFrom(credentials)
.flatMap(service.login) // login method has signature func login(_ creds: Credentials) -> Observable<User>
.subscribe(onNext: { user in print("Logged in \(user)") },
onError: { error in print("Received error") })
.disposed(by: disposeBag)
}
}
class ViewController: UIViewController {
let viewModel: ViewModel
let button = UIButton()
init(_ viewModel: ViewModel) {
self.viewModel = viewModel
}
}
In controller's viewDidLoad I make a binding:
override func viewDidLoad() {
button.rx.tap.asObservable()
.subscribe(viewModel.buttonDidTapSubject)
.disposed(by: disposeBag)
}
The problem is, since network request can fail and Observable that is returned from login(_:) method will produce an error, the whole subscription to buttonDidTapSubject in ViewModel will be disposed. And all other taps on a button will not trigger sequence to login in ViewModel.
Is there any way to avoid this kind of behavior?
You can use retry to prevent finishing the subcription. If you only want to retry in specific cases or errors you can also use retryWhen operator
In the view model:
lazy var retrySubject: Observable<Void> = {
return viewModel.buttonDidTapSubject
.retryWhen { error in
if (error == .networkError){ //check here your error
return .just(Void())
} else {
return .never() // Do not retry
}
}
}()
In the view controller I would have done it in another way:
override func viewDidLoad() {
super.viewDidLoad()
button.rx.tap.asObservable()
.flatMap { [weak self] _ in
return self?.viewModel.retrySubject
}
.subscribe(onNext: {
//do whatever
})
.disposed(by: disposeBag)
}
Not sure if still relevant - Use PublishRelay ( although it is RxCocoa )

How can I unit test that a block of code is run on DispatchQueue.main

Caveat - I read the few questions about testing threads but may have missed the answer so if the answer is there and I missed it, please point me in the right direction.
I want to test that a tableView call to reloadData is executed on the main queue.
This should code should result in a passing test:
var cats = [Cat]() {
didSet {
DispatchQueue.main.async { [weak self] in
tableView.reloadData()
}
}
}
This code should result in a failing test:
var cats = [Cat]() {
didSet {
tableView.reloadData()
}
}
What should the test look like?
Note to the testing haters: I know this is an easy thing to catch when you run the app but it's also an easy thing to miss when you're refactoring and adding layers of abstraction and multiple network calls and want to update the UI with some data but not other data etc etc... so please don't just answer with "Updates to UI go on the main thread" I know that already. Thanks!
Use dispatch_queue_set_specific function in order to associate a key-value pair with the main queue
Then use dispatch_queue_get_specific to check for the presence of key & value:
fileprivate let mainQueueKey = UnsafeMutablePointer<Void>.alloc(1)
fileprivate let mainQueueValue = UnsafeMutablePointer<Void>.alloc(1)
/* Associate a key-value pair with the Main Queue */
dispatch_queue_set_specific(
dispatch_get_main_queue(),
mainQueueKey,
mainQueueValue,
nil
)
func isMainQueue() -> Bool {
/* Checking for presence of key-value on current queue */
return (dispatch_get_specific(mainQueueKey) == mainQueueValue)
}
I wound up taking the more convoluted approach of adding an associated Bool value to UITableView, then swizzling UITableView to redirect reloadData()
fileprivate let reloadDataCalledOnMainThreadString = NSUUID().uuidString.cString(using: .utf8)!
fileprivate let reloadDataCalledOnMainThreadKey = UnsafeRawPointer(reloadDataCalledOnMainThreadString)
extension UITableView {
var reloadDataCalledOnMainThread: Bool? {
get {
let storedValue = objc_getAssociatedObject(self, reloadDataCalledOnMainThreadKey)
return storedValue as? Bool
}
set {
objc_setAssociatedObject(self, reloadDataCalledOnMainThreadKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
dynamic func _spyReloadData() {
reloadDataCalledOnMainThread = Thread.isMainThread
_spyReloadData()
}
//Then swizzle that with reloadData()
}
Then in the test I updated the cats on the background thread so I could check if they were reloaded on the main thread.
func testReloadDataIsCalledWhenCatsAreUpdated() {
// Checks for presence of another associated property that's set in the swizzled reloadData method
let reloadedPredicate = NSPredicate { [controller] _,_ in
controller.tableView.reloadDataWasCalled
}
expectation(for: reloadedPredicate, evaluatedWith: [:], handler: nil)
// Appends on the background queue to simulate an asynchronous call
DispatchQueue.global(qos: .background).async { [weak controller] in
let cat = Cat(name: "Test", identifier: 1)
controller?.cats.append(cat)
}
// 2 seconds seems excessive but NSPredicates only evaluate once per second
waitForExpectations(timeout: 2, handler: nil)
XCTAssert(controller.tableView.reloadDataCalledOnMainThread!,
"Reload data should be called on the main thread when cats are updated on a background thread")
}
Here is an updated version of the answer provided by Oleh Zayats that I am using in some tests of Combine publishers.
extension DispatchQueue {
func setAsExpectedQueue(isExpected: Bool = true) {
guard isExpected else {
setSpecific(key: .isExpectedQueueKey, value: nil)
return
}
setSpecific(key: .isExpectedQueueKey, value: true)
}
static func isExpectedQueue() -> Bool {
guard let isExpectedQueue = DispatchQueue.getSpecific(key: .isExpectedQueueKey) else {
return false
}
return isExpectedQueue
}
}
extension DispatchSpecificKey where T == Bool {
static let isExpectedQueueKey = DispatchSpecificKey<Bool>()
}
This is an example test using Dispatch and Combine to verify it is working as expected (you can see it fail if you remove the receive(on:) operator).:
final class IsExpectedQueueTests: XCTestCase {
func testIsExpectedQueue() {
DispatchQueue.main.setAsExpectedQueue()
let valueExpectation = expectation(description: "The value was received on the expected queue")
let completionExpectation = expectation(description: "The publisher completed on the expected queue")
defer {
waitForExpectations(timeout: 1)
DispatchQueue.main.setAsExpectedQueue(isExpected: false)
}
DispatchQueue.global().sync {
Just(())
.receive(on: DispatchQueue.main)
.sink { _ in
guard DispatchQueue.isExpectedQueue() else {
return
}
completionExpectation.fulfill()
} receiveValue: { _ in
guard DispatchQueue.isExpectedQueue() else {
return
}
valueExpectation.fulfill()
}.store(in: &cancellables)
}
}
override func tearDown() {
cancellables.removeAll()
super.tearDown()
}
var cancellables = Set<AnyCancellable>()
}

Resources