RxSwift properly dispose subscription in closure - ios

I am writing a wrapper around Firebase authentication functions to return Observable and add additional profileIncomplete state. It basically first checks whether a user is logged in, if so, check whether the user's profile is complete. The following is my code, I wonder whether it is okay to subscribe to an observable in Observable.create and, if so, how do I properly dispose the disposable in this case? create a DisposeBag inside the closure?
enum State {
case loggedIn
case profileIncomplete
case notLoggedIn
}
func listenToAuthState() -> Observable<State> {
return Observable.create { observable in
let authStateHandle = Auth.auth().addStateDidChangeListener() { [weak self] (_, user) in
guard let user = user else {
observable.onNext(.notLoggedIn)
return
}
let disposable = self?.listenToProfileCompleted(uid: user.uid).subscribe(onNext: { (completed) in
if completed {
observable.onNext(.loggedIn)
observable.onCompleted()
} else {
observable.onNext(.profileIncomplete)
}
})
// How to dispose the disposable???
}
return Disposables.create {
Auth.auth().removeStateDidChangeListener(authStateHandle) }
}
}
func listenToProfileCompleted(uid: String) -> Observable<Bool> { ... }

I think subscribing inside a Observable.create (or inside a different subscribe block) is a code-smell.
It seems you have two separate concerns. stateChanged and profileCompleted.
I would split those into two different methods, having listenToAuthState only in charge of reflecting the result of addStateDidChangeListener, and have a separate one for listenToProfileCompleted.
This will let you have a separate "ready" (or however you want to call it) that can zip the two. Or otherwise use flatMap, if the auth status must change before you listen to the profile completion.

To dispose resource you can add it to DisposeBag. Like below
func listenToAuthState() -> Observable<State> {
return Observable.create { observable in
var disposeBag:DisposeBag! = DisposeBag()
let authStateHandle = Auth.auth().addStateDidChangeListener() { [weak self] (_, user) in
guard let user = user else {
observable.onNext(.notLoggedIn)
return
}
let disposable = self?.listenToProfileCompleted(uid: user.uid).subscribe(onNext: { (completed) in
if completed {
observable.onNext(.loggedIn)
observable.onCompleted()
} else {
observable.onNext(.profileIncomplete)
}
}).disposed(by: disposeBag)
// How to dispose the disposable???
}
return Disposables.create {
Auth.auth().removeStateDidChangeListener(authStateHandle)
disposeBag = nil
}
}
}

Related

RxSwift set observable value in another observable next , not working

I am new to ios and rxswift. Trying create mvvm architecture for new app.
If I set observable isSuccess value before calling appStartNetwork.fetchApp() I can observe value. But when I set isSuccess value in fetchApp() on next, Observer in viewcontroller cant be triggered
Whats wrong?
ViewModel
class SplashViewModel {
var isSuccess = PublishSubject<Bool>()
var isLoading = PublishSubject<Bool>()
private let bag = DisposeBag()
func fetchAppStart() {
self.isLoading.onNext(true)
let appStartNetwork=NetworkProvider.shared.makeAppStartNetwork()
appStartNetwork.fetchApp().subscribe(onNext: { [weak self] apiResult in
switch apiResult{
case let .success(response):
//some codes
self?.isLoading.onNext(false)
self?.isSuccess.onNext(true)
break
case let .failure(errorContent):
break
}
},onError:{ err in
self.isLoading.onNext(false)
self.isSuccess.onNext(false)
}).disposed(by: bag)
} }
View Controller
func getAppStart(){
let splashVm=SplashViewModel()
let disposeBag = DisposeBag()
splashVm.isSuccess.subscribe(onNext: { (ok) in
if(ok){
print("splash success")
self.navigateMain()
}else{
self.showAlert("splash fail")
}
},onError:{ err in
self.showAlert(err.localizedDescription)
}).disposed(by: disposeBag)
splashVm.fetchAppStart()
}
There are two problems here, both created by the same programming error, a wrong management of the dispose bag lifecycles.
By creating your dispose bag within the scope of getAppStart, you are bounding its lifecycle to the function's life time. Meaning the dispose bag will dispose of its attached subscriptions when the function finishes.
Moving the creating of both disposeBag and splashVm to the view controller's scope (outside the function), should fix your issue.
let splashVm=SplashViewModel()
let disposeBag = DisposeBag()
func getAppStart(){
splashVm.isSuccess.subscribe(onNext: { (ok) in
if(ok){
print("splash success")
self.navigateMain()
}else{
self.showAlert("splash fail")
}
},onError:{ err in
self.showAlert(err.localizedDescription)
}).disposed(by: disposeBag)
splashVm.fetchAppStart()
}

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>()
}

RxSwift: Prevent multiple network requests

I am currently having an issue with multiple network requests executing when using RxSwift Observables. I understand that if one creates a cold observable and it has multiple observers, the observable will execute its block each time it is subscribed to.
I have tried to create a shared subscription observable that executes the network request once, and multiple subscribers will be notified of the result. Below is the what I have tried.
Sequence of events
Create the view model with the tap event of a uibutton
Create the serviceStatus Observable as a public property on the view model. This Observable is mapped from the buttonTapped Observable. It then filters out the "Loading" status. The returned Observable has a shareReplay(1) executed on it to return a shared subscription.
Create the serviceExecuting Observable as a public property on the view model. This observable is mapped from the serviceStatus Observable. It will return true if the status is "Loading"
Bind the uilabel to the serviceStatus Observable
Bind the activity indicator to the serviceExecuting Observable.
When the button is tapped, the service request is executed three time where I would be expecting it to be executed only once. Does anything stand out as incorrect?
Code
class ViewController {
let disposeBag = DisposeBag()
var button: UIButton!
var resultLabel: UILabel!
var activityIndicator: UIActivityIndicator!
lazy var viewModel = { // 1
return ViewModel(buttonTapped: self.button.rx.tap.asObservable())
}
override func viewDidLoad() {
super.viewDidLoad()
self.viewModel.serviceStatus.bindTo(self.resultLabel.rx_text).addDispsoableTo(disposeBag) // 4
self.viewModel.serviceExecuting.bindTo(self.activityIndicator.rx_animating).addDispsoableTo(disposeBag) // 5
}
}
class ViewModel {
public var serviceStatus: Observable<String> { // 2
let serviceStatusObseravble = self.getServiceStatusObservable()
let filtered = serviceStatusObseravble.filter { status in
return status != "Loading"
}
return filtered
}
public var serviceExecuting: Observable<Bool> { // 3
return self.serviceStatus.map { status in
return status == "Loading"
}
.startWith(false)
}
private let buttonTapped: Observable<Void>
init(buttonTapped: Observable<Void>) {
self.buttonTapped = buttonTapped
}
private func getServiceStatusObservable() -> Observable<String> {
return self.buttonTapped.flatMap { _ -> Observable<String> in
return self.createServiceStatusObservable()
}
}
private func createServiceStatusObservable() -> Observable<String> {
return Observable.create({ (observer) -> Disposable in
someAsyncServiceRequest() { result }
observer.onNext(result)
})
return NopDisposable.instance
})
.startWith("Loading")
.shareReplay(1)
}
EDIT:
Based on the conversation below, the following is what I was looking for...
I needed to apply a share() function on the Observable returned from the getServiceStatusObservable() method and not the Observable returned from the createServiceStatusObservable() method. There were multiple observers being added to this observable to inspect the current state. This meant that the observable executing the network request was getting executed N times (N being the number of observers). Now every time the button is tapped, the network request is executed once which is what I needed.
private func getServiceStatusObservable() -> Observable<String> {
return self.buttonTapped.flatMap { _ -> Observable<String> in
return self.createServiceStatusObservable()
}.share()
}
.shareReplay(1) will apply to only one instance of the observable. When creating it in createServiceStatusObservable() the sharing behavior will only affect the one value returned by this function.
class ViewModel {
let serviceStatusObservable: Observable<String>
init(buttonTapped: Observable<Void>) {
self.buttonTapped = buttonTapped
self.serviceStatusObservable = Observable.create({ (observer) -> Disposable in
someAsyncServiceRequest() { result in
observer.onNext(result)
}
return NopDisposable.instance
})
.startWith("Loading")
.shareReplay(1)
}
private func getServiceStatusObservable() -> Observable<String> {
return self.buttonTapped.flatMap { [weak self] _ -> Observable<String> in
return self.serviceStatusObservable
}
}
}
With this version, serviceStatusObservable is only created once, hence it's side effect will be shared everytime it is used, as it is the same instance.

RxSwift: using rx_refreshing for uirefreshcontrol

I am using the UIRefreshControl + Variable binding to reload data.
It is working, however, the following feels wrong to me:
1) I know there is a rx_refreshing variable in the RXCocoa extension, but I am unable to get it to work in this context.
2) I am binding answers (which is a Variable of array) twice. Once when I load the view controller and again when the UIRefreshControl is refreshing.
3) The parts where I check for whether the UIRefreshControl is refreshing or not looks really awkward. It feels like it defeats the purpose of using reactive?
...
let answers: Variable<[Answer]> = Variable([])
override func viewDidLoad() {
loadAnswers()
.shareReplay(1)
.bindTo(answers)
.addDisposableTo(self.disposeBag)
setupRx()
}
func loadAnswers() -> Observable<[Answer]> {
return Network.rxArrayRequest(Spark.Answers)
}
func setupRx() {
rc.rx_controlEvent(.ValueChanged)
.map { _ in !self.rc.refreshing }
.filter { $0 == false }
.flatMapLatest { [unowned self] _ in
return self.loadAnswers()
}
.bindTo(answers)
.addDisposableTo(self.disposeBag)
rc.rx_controlEvent(.ValueChanged)
.map { _ in self.rc.refreshing }
.filter { $0 == true }
.subscribeNext { [unowned self] _ in
self.rc.endRefreshing()
}
.addDisposableTo(self.disposeBag)
}
...
So first of all, It's not actually working. It just seems to be working. In your code, you're actually not waiting for the network request to finish before you call rc.endRefreshing(). Instead, you're just making the network call and then immediately calling endRefreshing().
// `rc.rx_controlEvent(.ValueChanged)` only gets called once,
// when the user pulls down.
rc.rx_controlEvent(.ValueChanged) // user pulled down to refresh
.map { _ in !self.rc.refreshing } // !true -> false
.filter { $0 == false } // false == false
.flatMapLatest { [unowned self] _ in
return self.loadAnswers() // request answers
}
.bindTo(answers)
.addDisposableTo(self.disposeBag)
rc.rx_controlEvent(.ValueChanged) // user pulled down to refresh
.map { _ in self.rc.refreshing } // true -> true
.filter { $0 == true } // true == true
.subscribeNext { [unowned self] _ in
self.rc.endRefreshing() // end refreshing
}
.addDisposableTo(self.disposeBag)
To address concern 1, you're right, you can use rx_refreshing to turn off refreshing instead of endRefreshing().
To address concern 2, I don't think the Variable is necessary or useful, at least in this example. You could still use it though. Also, it's not necessary to loadAnswers() in two places.
To address concern 3, yea, you could be simplifying this a lot and using Rx a bit more.
Here's code that would actually work, use rx_refreshing, and simplify things a lot:
let initial = Observable<Void>.just(())
let refresh = rc.rx_controlEvent(.ValueChanged).map { _ in () }
let answers = Observable.of(initial, refresh)
.merge()
.flatMapLatest{ _ in self.loadAnswers() }
.shareReplayLatestWhileConnected()
answers
.map { _ in false }
.bindTo(rc.rx_refreshing)
.addDisposableTo(disposeBag)
// also use `answers` to bind to your data source, etc.

RxSwift: Return a new observable with an error

I have a function that return a Bool Observable depending if it was ok or not.
func test() -> Observable<Bool> {
if everythingIsOk {
return just(true)
}
return just(false) <- how can i here return a custom error to retrieve what failed?
}
just<E>(element: E) -> Observable<E>
Returns an observable sequence that contains a single element.
Instead, you should use something like that:
create<E>(subscribe: (AnyObserver<E>) -> Disposable) -> Observable<E>
Create method creates an observable sequence from a specified subscribe method implementation.
In your case:
private let realm = try! Realm()
func save(customObject: CustomObject) -> Observable<Bool> {
return create({ observer -> Disposable in
do {
try self.realm.write {
self.realm.add(customObject, update: true)
observer.onNext(true)
observer.onCompleted()
}
} catch {
// .Error sequence will be automatically completed
observer.onError(NSError(domai...)
}
// if realm.write is sync task(by default it is, as I know) you can actually return NopDisposable
return NopDisposable.instance
// otherwise you should cancel write transaction in AnonymousDisposable
})
}
AnonymousDisposable is the action that’s called in case you want to get interrupted. Say you leave your view controller or the app needs to be done with the service and you don’t need to call this request any longer. It’s great for video uploads or something much larger. You can do request.cancel() which cleans up all the resources when you’re done with it. This gets called on either completion or error.
For creating observables there is create function. You can use it like this:
func test() -> Observable<Bool> {
return create({ (observer) -> Disposable in
// Some condition
observer.onNext(true)
// Some other condition
observer.onNext(false)
// Some other condition
observer.onError(NSError(domain: "My domain", code: -1, userInfo: nil))
// Some other condition
observer.onCompleted()
return AnonymousDisposable {
// Dispose resources here
}
// If u have nothing to dipose use NopDisposable.instance
})
}
Use a result enum as your observable value.
public enum Result<Value> {
case success(Value)
case failure(Error)
}
func test() -> Observable<Result<Bool>> {
if everythingIsOk {
return just(.success(true))
}
let error = ...
return just(.failure(error))
}

Resources