Below is the example code of RxAlamofire network request. My problem is that I want to cancel this request whenever the View Controller is dismissed.
I tried to assign this request to a global variable but requestJSON method returns Observable<(HTTPURLResponse, Any)> type.
Is there a way to handle this request when the View Controller is dismissed?
RxAlamofire.requestJSON(.get, sourceStringURL)
.debug()
.subscribe(onNext: { [weak self] (r, json) in
if let dict = json as? [String: AnyObject] {
let valDict = dict["rates"] as! Dictionary<String, AnyObject>
if let conversionRate = valDict["USD"] as? Float {
self?.toTextField.text = formatter
.string(from: NSNumber(value: conversionRate * fromValue))
}
}
}, onError: { [weak self] (error) in
self?.displayError(error as NSError)
})
.disposed(by: disposeBag)
If you look at RxAlamofire's code:
https://github.com/RxSwiftCommunity/RxAlamofire/blob/8a4856ddd77910950aa2b0f9e237e0209580503c/Sources/RxAlamofire.swift#L434
You'll see that the request is cancelled when the subscription is disposed.
So as long as your view controller is released (and its dispose bag with it!) when you dismiss it then the request will be cancelled if it hasn't finished of course.
As Valérian points out, when your ViewController is dismissed, it and all its properties will be deallocated (if retain count drops to 0 that is).
In particular, when disposeBag property is deallocated, dispose() will be called on all observable sequences added to this bag. Which, in turn, will call request.cancel() in RxAlamofire implementation.
If you need to cancel your request earlier, you can try nil'ing your disposeBag directly.
Related
I have set up a relay like this:
class CustomAlertViewController: UIViewController {
private let disposeBag = DisposeBag()
var alertTextRelay = BehaviorRelay<String>(value: "")
alertTextField.rx.value.orEmpty.changed.subscribe(onNext: { [weak self] value in
print("THIS IS RIGHT", value)
self?.alertTextRelay.accept(value)
print("THIS IS ALSO RIGHT", self?.alertTextRelay.value)
}).disposed(by: disposeBag)
...
I get the text writen in the textField and accept it to the relay and check so that the value is really there. But the subscription never trigger:
class RecievingClass: UIViewController {
let disposeBag = DisposeBag()
let instance = CustomAlertViewController()
instance.alertTextRelay.subscribe(onNext: { value in
self.myText = value
}, onError: { error in
print(error)
}, onCompleted: {
print("completed")
}).disposed(by: disposeBag)
...
Nothing in the subscription is triggered. Why? If it helps, CustomAlertViewController is used as a custom alert (a view on an overlay). Also, there are a lot of static functions in CustomAlertViewController. Not sure if that's relevant. Let me know if I can provide anything else.
Try to change this alertTextField.rx.value.orEmpty.changed.subscribe on this alertTextField.rx.text.orEmpty.asObservable(). And add [weak self] for RecievingClass in other way you get memory leak.
I have a bluetooth class which passes when a char value is updated to a closure in a view controller (as well as the same closure in a singleton class). when the VC deinit is called, the closure in the VC is still being executed when the char value is updated. I am using [weak self] for the closure in the VC. I'd like to be able to stop this VC closure from being called when the view is deinitialised. But I also don't understand why the other callback in the singleton is not being executed after the VC is presented!
Included below is the syntax for the closure inside the VC
bluetooth.updatedCharacteristicsValue { [weak self] char in
[weak self] does not mean that the closure can be discarded, it only prevents the closure from retaining the VC (and therefore preventing the VC from being deinited).
Simply begin your closure with:
guard let self = self else { return }
... to exit early if the VC no longer exists.
As for why the closure supplied by the VC is being called but the one in the singleton isn't, it sounds like your bluetooth class doesn't understand the concept of multiple 'users'. Whoever registers their callback last is the one that is called.
An approach to handling your own observer registration with convenient self-unregistering tokens:
class ObserverToken {
let id = UUID()
private let onDeinit: (UUID) -> ()
init(onDeinit: #escaping (UUID) -> ()) {
self.onDeinit = onDeinit
}
deinit {
onDeinit(id)
}
}
class BluetoothThing {
// Associate observers with the .id of the corresponding token
private var observers = [UUID: (Int) -> ()]()
func addObserver(using closure: #escaping (Int) -> ()) -> ObserverToken {
// Create a token which sets the corresponding observer to nil
// when it is deinit'd
let token = ObserverToken { [weak self] in self?.observers[$0] = nil }
observers[token.id] = closure
return token
}
func tellObserversThatSomethingHappened(newValue: Int) {
// However many observers we currently have, tell them all
observers.values.forEach { $0(newValue) }
}
deinit {
print("đź‘‹")
}
}
// I've only made this var optional so that it can later be set to nil
// to prove there's no retain cycle with the tokens
var bluetooth: BluetoothThing? = BluetoothThing()
// For as long as this token exists, updates will cause this closure
// to be called. As soon as this token is set to nil, it's deinit
// will automatically deregister the closure
var observerA: ObserverToken? = bluetooth?.addObserver { newValue in
print("Observer A saw: \(newValue)")
}
// Results in:
// Observer A saw: 42
bluetooth?.tellObserversThatSomethingHappened(newValue: 42)
// A second observer
var observerB: ObserverToken? = bluetooth?.addObserver { newValue in
print("Observer B saw: \(newValue)")
}
// Results in:
// Observer A saw: 123
// Observer B saw: 123
bluetooth?.tellObserversThatSomethingHappened(newValue: 123)
// The first observer goes away.
observerA = nil
// Results in:
// Observer B saw: 99
bluetooth?.tellObserversThatSomethingHappened(newValue: 99)
// There is still one 'live' token. If it is retaining the
// Bluetooth object then this assignment won't allow the
// Bluetooth to deinit (no wavey hand)
bluetooth = nil
So if your VC stores it's token as a property, when the VC goes away, the token goes away and the closure is deregistered.
I can't figure out if I need to use [weak self] in this situation or not ?
HTTPClient.swift:
struct HTTPClient {
let session = URLSession.shared
func get(url: URL, completion: #escaping (Data) -> Void) {
session.dataTask(with: url) { data, urlResponse, error in
completion(data) // assume everything will go well
}.resume()
}
}
Service.swift
struct Service {
let httpClient: HTTPClient
init(httpClient: HTTPClient = HTTPClient()) {
self.httpClient = httpClient
}
func fetchUser(completion: #escaping (User) -> Void) {
httpClient.get("urlToGetUser") { data in
// transform data to User
completion(user)
}
}
}
ViewModel.swift
class ViewModel {
let service: Service
let user: User?
var didLoadData: ((User) -> Void)?
init(service: Service) {
self.service = service
loadUser()
}
func loadUser() {
service.fetchUser { [weak self] user in // is [weak self] really needed ?
self?.user = user
self?.didLoadData?(user)
}
}
}
is it really needed here to user [weak self] ? and is there a rule on how to check if it's needed in general when we deal with an API that we don't know what's happening to the closure ? or that does not matter (it's up to us to decide) ?
In the example you've given, [weak self] is potentially unnecessary. It depends on what you want to happen if ViewModel is released before the request completes.
As noted in the URLSessionDataTask docs (emphasis mine):
After you create a task, you start it by calling its resume() method. The session then maintains a strong reference to the task until the request finishes or fails; you don’t need to maintain a reference to the task unless it’s useful for your app’s internal bookkeeping.
The session has a strong reference to the task. The task has a strong reference to the closure. The closure has a strong reference to the ViewModel. As long as the ViewModel doesn't have a strong reference to the task (which it doesn't in the code you provided), then there's no cycle.
The question is whether you want to ensure that the ViewModel continues to exist long enough for the closure to execute. If you do (or don't care), then you can use a simple strong reference. If you want to prevent the task from keeping the ViewModel alive, then you should use a weak reference.
This is how you need to think about reference cycles. There is no general rule "use weak here." You use weak when that's what you mean; when you don't want this closure to keep self around until it is released. That particularly is true if it creates a cycle. But there's no general answer for "does this create a cycle." It depends on what pieces hold references.
This also points to where your current API design is not as good as it could be. You're passing didLoadData in init. That very likely is going to create reference cycles and force your caller to use weak. If instead you made didLoadDdata a completion handler on loadUser(), then you could avoid that problem and make life easier on the caller.
func loadUser(completion: #escaping ((User?) -> Void)? = nil) {
service.fetchUser {
self?.user = user
didLoadData?(user)
}
}
(Your current API has a race condition in any case. You're starting loadUser() before didLoadData can be set. You may be assuming that the completion handler won't complete before you've set dataDidLoad, but there's no real promise of that. It's probably true, but it's fragile at best.)
The problem with your code is not with the use of URLSession but with the fact that you are retaining a function in your view controller:
class ViewModel {
var didLoadData: ((User) -> Void)?
}
If the didLoadData function mentions self (i.e. the ViewModel instance) implicitly or explicitly, you have a retain cycle and a memory leak unless you say weak self or unowned self.
I'm trying to achieve my Observables to execute only when previous Observable has completed. I can't use flatMap, because subscriptions can be called from different places, and this Observables is not connected with each other. To be specific: I have my CollectionView loading more content from server and 2 seconds after that user clicks "Send comment" button while CollectionView is still loading its batch. So I want to wait until CollectionView update completes and only then execute my comment's posting request. I created a class named ObservableQueue and it's working just fine. But I need to know if it has issues like memory leaks, dead locks or maybe I just missing something. Here it is:
extension CompositeDisposable {
#discardableResult
func insert(disposeAction: #escaping () -> ()) -> DisposeKey? {
return insert(Disposables.create(with: disposeAction))
}
}
class ObservableQueue {
private let lock = NSRecursiveLock()
private let relay = BehaviorRelay(value: 0)
private let scheduler = SerialDispatchQueueScheduler(internalSerialQueueName: "ObservableQueue.scheduler")
func enqueue<T>(_ observable: Observable<T>) -> Observable<T> {
return Observable.create({ observer -> Disposable in
let disposable = CompositeDisposable()
let relayDisposable = self
.relay
.observeOn(self.scheduler)
.filter({ value -> Bool in
if value > 0 {
return false
}
self.lock.lock(); defer { self.lock.unlock() }
if self.relay.value > 0 {
return false
}
self.relay.accept(self.relay.value + 1)
disposable.insert {
self.lock.lock(); defer { self.lock.unlock() }
self.relay.accept(self.relay.value - 1)
}
return true
})
.take(1)
.flatMapLatest { _ in observable }
.subscribe { observer.on($0) }
_ = disposable.insert(relayDisposable)
return disposable
})
}
}
And then I can use it like this:
let queue = ObservableQueue()
...
// first observable
let observable1 = Observable
.just(0)
.delay(5, scheduler: MainScheduler.instance)
queue
.enqueue(observable1)
.subscribe(onNext: { _ in
print("here1")
})
.disposed(by: rx.disposeBag)
// second observable
let observable2 = Observable
.just(0)
.delay(5, scheduler: MainScheduler.instance)
queue
.enqueue(observable2)
.subscribe(onNext: { _ in
print("here2")
})
.disposed(by: rx.disposeBag)
// third observable
let observable3 = Observable
.just(0)
.delay(5, scheduler: MainScheduler.instance)
queue
.enqueue(observable3)
.subscribe(onNext: { _ in
print("here3")
})
.disposed(by: rx.disposeBag)
CLGeocoder has the same issue. According to the documentation, you can't call one of the geocoder methods while it's working on a previous request so very much like what you are trying to do. In this gist (https://gist.github.com/danielt1263/64bda2a32c18b8c28e1e22085a05df5a), you will find that I make the observable calls on a background thread and protect the job with semaphore. That's the key, you need a semaphore, not a lock.
Something like this should work for you:
class ObservableQueue {
private let semaphore = DispatchSemaphore(value: 1)
private let scheduler = ConcurrentDispatchQueueScheduler(qos: .userInitiated)
func enqueue<T>(_ observable: Observable<T>) -> Observable<T> {
let _semaphore = semaphore // To avoid the use of self in the block below
return Observable.create { observer in
_semaphore.wait()
let disposable = observable.subscribe { event in
switch event {
case .next:
observer.on(event)
case .error, .completed:
observer.on(event)
}
}
return Disposables.create {
disposable.dispose()
_semaphore.signal()
}
}
.subscribeOn(scheduler)
}
}
I will give you some suggestions that I think will help you in the future.
Avoid as much as possible the Observable.create, this is the "brute force" creation of an observable and it doesn't handle back pressure at all, you'll have to implement it yourself, and it's not something easy.
Usually for HTTP api calls, you don't need Observable, you should use Single or Completable since you expect only one response from your server, not a stream of responses.
You should be careful with strong self inside the onNext/on..., as a rule of thumb if the class that subscribes to the observer has the dispose bag, you should use a weak self.
Now for your particular case, if you need to just this pair of observers (fetch & send comment), I think the queue is a little bit overkill. You can simply call the post comment observer (if available) on the do(onNext:) method of your "fetch" observer. Do on next is called every time an "onNext" event is triggered.
If you still need a queue, I would go with an OperationQueue that enqueues only operations and has a method like observeOperationchanges() -> Observeble<Operation> this will be triggered every time an operation is completed. In this way you subscribe once and enqueue multiple times, but this might not fit your needs.
I would use .combineLatest() to produce an event once both observables have emitted something. See http://rxmarbles.com/#combineLatest
The question is - where it’s best to:
Call error handling popups
Show/Hide loading indicator
My app looks like this:
ViewController that subscribes to trigger of UI update when the model changes:
var viewModel: ViewModel = ViewModel()
...
viewModel.source.asObservable().subscribe(onNext: { (_ ) in
self.tableView.reloadData()
})
.disposed(by: bag)
ViewModel
var source = Variable<[Student]>([])
And when initialized it fetches the source output
api.fetchSourceOutput(id: id)
.do(onError: { (error) in
//show error here???
})
.catchErrorJustReturn([])
.bind(to: source)
.disposed(by: bag)
I can't just pass reference of ViewController into ViewModel, that would break the idea of it's independence from UI. Then how am I supposed to call error popup in view controller's view? Getting top view controller is not a good option either, because I might need specific view to show my popup in.
The loading indicator can be shown when onNext called inside viewModel and hidden onCompleted. But I again don't have reference to my view controller where my loading indicator reference resides.
Ideas?
Call error handling popups
Lets say you have some signal which starts api fetch
let someSignalWithIdToStartApiFetch = Observable.just(1)
Also, lets imagine that when you present some "retry request" popup on error and when user clicks on "retry" button you bind it to some observer. Then convert an observer to Observable. So you have some 2nd signal:
let someSignalWhenUserAsksToRetryRequestAfterError = Observable.just(())
When you need to retry a request you take the last id from someSignalWithIdToStartApiFetch this way:
let someSignalWithIdToRetryApiFetch = someSignalWhenUserAsksToRetryRequestAfterError
.withLatestFrom(someSignalWithIdToStartApiFetch)
.share(replay: 1, scope: .whileConnected)
Then you combine both signals and make a request:
let apiFetch = Observable
.of(someSignalWithIdToRetryApiFetch, someSignalWithIdToStartApiFetch)
.merge()
.flatMap({ id -> Observable<Response> in
return api
.fetchSourceOutput(id: id)
.map({ Response.success($0) })
.catchError({ Observable.just(Response.error($0)) })
})
.share(replay: 1, scope: .whileConnected)
As you can see, the error is caught and converted to some result. For example:
enum Response {
case error(Error)
case success([Student])
var error: Error? {
switch self {
case .error(let error): return error
default: return nil
}
}
var students: [Student]? {
switch self {
case .success(let students): return students
default: return nil
}
}
}
Then you work with successful result as usual:
apiFetch
.map({ $0.students })
.filterNil()
.bind(to: source)
.disposed(by: bag)
But the error case should be bind to some observer which triggers popup to be shown:
apiFetch
.map({ $0.error })
.filterNil()
.bind(to: observerWhichShowsPopUpWithRetryButton)
.disposed(by: bag)
So, when the pop up is shown and user clicks on "retry" - someSignalWhenUserAsksToRetryRequestAfterError will trigger and retry the request
Show/Hide loading indicator
I use something like this. It is a special structure which catches the activity of an observable. How you can use it?
let indicator = ActivityIndicator()
And some code from 1st part of the question.
let apiFetch = Observable
.of(someSignalWithIdToRetryApiFetch, someSignalWithIdToStartApiFetch)
.merge()
.flatMap({ id -> Observable<[Student]> in
return indicator
.trackActivity(api.fetchSourceOutput(id: id))
})
.map({ Response.success($0) })
.catchError({ Observable.just(Response.error($0)) })
.share(replay: 1, scope: .whileConnected)
So, the activity of api fetch is tracked. Now you should show/hide your activity view.
let observableActivity = indicator.asObservable() // Observable<Bool>
let observableShowLoading = observableActivity.filter({ $0 == true })
let observableHideLoading = observableActivity.filter({ $0 == false })
Bind observableShowLoading and observableHideLoading to hide/show functions. Even if you have multiple request which might be executed simultaneously - bind them all to a single ActivityIndicator.
Hope it helps. Happy coding (^
I would make this changes in your viewModel:
// MARK: - Properties
let identifier = Variable(0)
lazy var source: Observable<[Student]> = identifier.asObservable()
.skip(1)
.flatMapLatest { id in
return api.fetchSourceOutput(id: id)
}
.observeOn(MainScheduler.instance)
.share(replay: 1)
...
// MARK: - Initialization
init(id: Int) {
identifier.value = id
...
}
Then, in your ViewController:
viewModel.source
.subscribe(onNext: { _ in
self.tableView.reloadData()
}, onError: { error in
// Manage errors
})
.disposed(by: bag)