RxSwift | Initialize and push UIViewController in closure - ios

For example we have three UIViewControllers:
A, B, C.
We pushing B from A.
In B we calling some API:
func getProduct(productNumber: String) {
someService.rxGetProduct(productNumber: productNumber)
.asObservable()
.trackActivity(loading)
.subscribe(onNext: { [weak self] product in
guard let `self` = self else { return }
let cViewModel: CViewModel = .init()
let cViewController: CViewController = .init(viewModel: cViewModel)
self.navigationController?.pushViewController(cViewController, animated: true)
}, onError: { [weak self] error in
// error handling
}).disposed(by: disposeBag)
}
In method above, we are getting some product model and pushing C view controller in closure.
The problem is, when we are popping back from C view controller to B view controller – C view controller is not deinitializing. C view controller is deinitializing when we are popping back from B view controller to A view controller.
What I am doing wrong?

You might be creating the retain cycle within trackactivity or whatever loading is
consider using take(1) and asSingle() in case your observable is not intended to complete.

Related

RxSwift concurrency problems with coordinator

What I want to do:
Present VC1
When VC1 is dismissed, present VC2
Problem:
When VC1 is dismissed, VC2 does not present
Dirty Fix:
Put milisecond delay. It fixes the problem, but want to know why it happens
Explanation: I get viewDidDissapear event when VC1 dismisses so I can present VC2
If you need more details, please ask.
Code:
class ViewModel {
let coordinator = Coordinator()
struct Input {
let itemSelected: Driver<IndexPath>
}
struct Output {
let presentVC1: Driver<Void>
let presentVC2: Driver<Void>
}
func transform(input: Input) -> Output {
let navigateToVC1 = input.itemSelected
.flatMap { [coordinator] in
return coordinator.transition(to: Scene.VC1)
}
let navigateToVC2 = navigateToVC1
.delay(.milliseconds(1))
.flatMap { [coordinator] in
return coordinator.transition(to: Scene.VC2)
}
return Output(presentVC1: presentVC1, presentVC2: presentVC2)
}
Coordinator code:
func transition(to scene: TargetScene) -> Driver<Void> {
let subject = PublishSubject<Void>()
switch scene.transition {
case let .present(viewController):
_ = viewController.rx
.sentMessage(#selector(UIViewController.viewDidDisappear(_:)))
.map { _ in }
.bind(to:subject)
currentViewController.present(viewController, animated: true)
return subject
.take(1)
.asDriverOnErrorJustComplete()
}
The viewDidDisappear method is called before the view controller is fully dismissed. You should not try to present the second view controller until the callback of dismiss is called.
Wherever you are dismissing your view controller, use the below instead and don't present the next view controller until after the observable emits a next event.
extension Reactive where Base: UIViewController {
func dismiss(animated: Bool) -> Observable<Void> {
Observable.create { [base] observer in
base.dismiss(animated: animated) {
observer.onNext(())
observer.onCompleted()
}
return Disposables.create()
}
}
}
I suggest you consider using my Cause-Logic-Effect architecture which contains everything you need to properly handle view controller presentation and dismissal.
https://github.com/danielt1263/CLE-Architecture-Tools
A portion of the interface is below:
/**
Presents a scene onto the top view controller of the presentation stack. The scene will be dismissed when either the action observable completes/errors or is disposed.
- Parameters:
- animated: Pass `true` to animate the presentation; otherwise, pass `false`.
- sourceView: If the scene will be presented in a popover controller, this is the view that will serve as the focus.
- scene: A factory function for creating the Scene.
- Returns: The Scene's output action `Observable`.
*/
func presentScene<Action>(animated: Bool, overSourceView sourceView: UIView? = nil, scene: #escaping () -> Scene<Action>) -> Observable<Action>
extension NSObjectProtocol where Self : UIViewController {
/**
Create a scene from an already existing view controller.
- Parameter connect: A function describing how the view controller should be connected and returning an Observable that emits any data the scene needs to communicate to its parent.
- Returns: A Scene containing the view controller and return value of the connect function.
Example:
`let exampleScene = ExampleViewController().scene { $0.connect() }`
*/
func scene<Action>(_ connect: (Self) -> Observable<Action>) -> Scene<Action>
}
struct Scene<Action> {
let controller: UIViewController
let action: Observable<Action>
}
The connect function is your view model, when its Observable completes or its subscriber disposes, the view controller will automatically dismiss.
The presentScene function is your coordinator. It handles the actual presenting and dismissing of scenes. When you dismiss and present a new scene, it will properly handle waiting until the previous view controller is dismissed before presenting the next one.

Swift closure is still in memory after VC deinit is called

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.

RxAlamofire cancel network request

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.

how can I instantiate a viewController with a containerView and it's containerView at the same time?

I want to instantiate a viewController with a container with the following:
let vc = self.storyboard?.instantiateViewController(withIdentifier: ContainerViewController") as? ContainerViewController
I also need a reference to the containerView so I try the following:
let vc2 = vc.childViewControllers[0] as! ChildViewController
The app crashes with a 'index 0 beyond bounds for empty NSArray'
How can I instantiate the containerViewController and it's childViewController at the same time prior to loading the containerViewController?
EDIT
The use case is for AWS Cognito to go to the signInViewController when the user is not authenticated. This code is in the appDelegate:
func startPasswordAuthentication() -> AWSCognitoIdentityPasswordAuthentication {
if self.containerViewController == nil {
self.containerViewController = self.storyboard?.instantiateViewController(withIdentifier: "ContainerViewController") as? ContainerViewController
}
if self.childViewController == nil {
self.childViewController = self.containerViewController!.childViewControllers[0] as! ChildViewController
}
DispatchQueue.main.async {
self.window?.rootViewController?.present(self.containerViewController!, animated: true, completion: nil)
}
return self.childViewController!
}
The reason I am instantiating the container and returning the child is that the return needs to conform to the protocol which only the child does. I suppose I can remove the container but it has functionality that I would have wanted.
Short answer: You can't. At the time you call instantiateViewController(), a view controller's view has not yet been loaded. You need to present it to the screen somehow and then look for it's child view once it's done being displayed.
We need more info about your use-case in order to help you.
EDIT:
Ok, several things:
If your startPasswordAuthentication() function is called on the main thread, there's no reason to use DispatchQueue.main.async for the present() call.
If, on the other hand, your startPasswordAuthentication() function is called on a background thread, the call to instantiateViewController() also belongs inside a DispatchQueue.main.async block so it's performed on the main thread. In fact you might just want to put the whole body of your startPasswordAuthentication() function inside a DispatchQueue.main.async block.
Next, there is no way that your containerViewController's child view controllers will be loaded after the call to instantiateViewController(withIdentifier:). That's not how it works. You should look for the child view in the completion block of your present call.
Next, you should not be reaching into your containerViewController's view hierarchy. You should add methods to that class that let you ask for the view you are looking for, and use those.
If you are trying to write your function to synchronously return a child view controller, you can't do that either. You need to rewrite your startPasswordAuthentication() function to take a completion handler, and pass the child view controller to the completion handler
So the code might be rewritten like this:
func startPasswordAuthentication(completion: #escaping (AWSCognitoIdentityPasswordAuthentication?)->void ) {
DispatchQueue.main.async { [weak self] in
guard strongSelf = self else {
completion(nil)
return
}
if self.containerViewController == nil {
self.containerViewController = self.storyboard?.instantiateViewController(withIdentifier: "ContainerViewController") as? ContainerViewController
}
self.window?.rootViewController?.present(self.containerViewController!, animated: true, completion: {
if strongSelf == nil {
strongSelf.childViewController = self.containerViewController.getChildViewController()
}
completion(strongSelf.childViewController)
}
})
}
(That code was typed into the horrible SO editor, is totally untested, and is not meant to be copy/pasted. It likely contains errors that need to be fixed. It's only meant as a rough guide.)

Pushing Navigation Controller From Other Class in Swift

I have a Data Manager class that handles some JSON activities. When one action is completed, I want to push to the Navigation controller.
The FirstViewController calls a function on the DataManager Class
DataManager Class
class func extract_json(data:NSData) -> Bool {
//Success
FirstViewController().pushToValueView()
}
FirstViewController Class
func pushToValueView() {
println("Push to Value View")
navigationController?.pushViewController(ValueViewController(), animated: true)
}
In this example, println() is called but no push occurs. If I call pushToValueView() within FirstViewController e.g. self.pushToValueView() (For debug purposes), a push does occur.
Try calling this function on an existing instance of FirstViewController.
I think that in this example you try to push view controller to FirstViewController which is deallocated after exiting the method scope. That is why view controller doesn't appear
Unwrap the optional. Edit: You shouldn't call your vc from a model class btw, thats bad MVC, I'd use an observers rather and get notified when the closure is complete
if let nav = self.navigationController {
println("Push to Value View")
nav.pushViewController(ValueViewController(), animated: true)
}
You can modify your extract_json function so that it accepts a closure to be executed once the data has been extracted. The closure will contain the code to push the new view controller and it will execute in the context of the calling rather than called object:
In DataManager.swift -
func extractJSON(data:NSData, completion:() -> Void) {
//Do whatever
completion()
}
In FirstViewController.swift -
func getDataAndSegue() {
extractJSON(someData, completion:{
navigationController?.pushViewController(ValueViewController(), animated: true)
}
)
}

Resources