dismiss viewController always trigger viewDidLoad and never viewWillAppear - ios

I have a tab bar, in the last tab I have a UIViewController A, that have a button inside, and when you tap on it it is presenting an UIViewController B :
let bVC = B()
bVC.settingsPresenter = self
self.present(bVC, animated: true, completion: nil)
When the user is in B and wants to get out of the screen, there is a button that calls this method on A (not B) :
self.dismiss(animated: true, completion: nil)
So when I arrive on the tab for the first time, it calls viewDidLoad then viewWillAppear on A.
If I go to the first tab then come back to the last, only viewWillAppear on A. Everything looks normal.
But when I dismiss B, viewDidLoad of A is called, and not even viewWillAppear... how can I make it the inverse (you know the logical way as the view is already loaded and I only need to be notify that the tab will appear)

if the ViewController is loading but showing another one, it will never view will appear as the other view controller it is presenting will appear
Hope this helps!

Related

How to avoid memory leaks while presenting / segueing to new View Controllers

In my app I have a MainViewController where user can add, remove and reorder items in CollectionView. Each Collection View Cell initiates segue to a new VC (n-th VC in the image below). From the newly presented vc user can go back to the MainVC or segue to the next (or previous) VC that's also accessible from the MainVCs Collection View.
Since the order of items in Collection View is dynamic I'll have instantiate the next VC and then present it:
let nextVC = storyboard?.instantiateViewController(withIdentifier: "NextVC")
present(nextVC!, animated: true, completion: nil)
My question is (are):
Do I have to worry about a memory leaks if I use unwind segue when
the user taps on goBackToMainVC (I.e. will all previously
instantiated VCs be automatically dismissed?)
If the user decides not to go back to the MainVC, should I dismiss the current VC before presenting a new one? If so, where should I call the dismiss function?
Edit: Additional question:
Would adding
if (presentingViewController?.restorationIdentifier != "MainVC") {
presentingViewController?.dismiss(animated: false, completion: nil)
}
to every VC accessible from the collection view in MainVC solve my problem?
If you are dismissing or unwinding back to the main controller, it would deallocate the loaded view controller(s) that was loaded when presenting. That is, providing there are no strong references to anything to those view controllers.
To verify the view controllers are being deallocated, you can print something to the console when the controllers are dismissed via deinit.
deinit {
print("deinit called")
}
An unwind segue from a Present Modally segue calls dismiss. That is what an unwind segue is. So there is no special memory management associated with an unwind segue.

testing "presentingViewController is UIViewController" works fine in one case, fails in other

Following Apple's documentation for adding and editing information Apple guide here I have a Viewcontroller with a tableview. The tableview contains a header with a "Add new" Button. If a table row is selected the detailViewController is pushed onto the stack. The detailViewController is also embedded in a UINavigationController, as in Apple's docs. If "Add new" is pressed, another segue is performed which presents the UINavigationController modally, which in turns shows the detailViewController. This works fine and the animation clearly shows a modally presented ViewController.
The detailViewController contains a Cancel Button in the NavigationBar. If it is pressed the following code is run:
#IBAction func cancel(_ sender: UIBarButtonItem) {
// Depending on style of presentation (modal or push presentation), this view controller needs to be dismissed in two different ways.
var isPresentingInAddActionMode = false
if let presentingVC = self.presentingViewController{
isPresentingInAddActionMode = presentingVC is UINavigationController
}
streekgidsModel.undoManager.endUndoGrouping()
print("undo grouping ended and undone")
streekgidsModel.undoManager.undo()
if isPresentingInAddActionMode {
dismiss(animated: true, completion: nil)
}
else if let owningNavigationController = navigationController{
owningNavigationController.popViewController(animated: true)
}
else {
fatalError("The MealViewController is not inside a navigation controller.")
}
}
The first if-statement checks if the property presentingViewController is present, and if so if it is of type UINavigationController. If so, the viewController is presented modally and should be dismissed. If not it is pushed onto the stack and the owningNavigationController should pop the detailViewController.
Running this code does not work as described by Apple. The check on the presentingViewController shows it is present, but the type check gives back "invalid". This is treated as false. The test on the owningNavigationController succeeds (I think it should fail) and the popViewController is executed. As there was no push, the view controller is not popped or dismissed and is still visible. A second press on Cancel executes the func cancel again, which results in an error as there is no longer a group started in the undo manager.
Baffling thing is that I have the same code in another viewcontroller, with similar UIViewTable and navigation and it works fine.
So to frame the question: why does this not work the way Apple describes it, why does my other view controller work as it is supposed to? Any input is appreciated.
BTW, the fatal error text is straight from the docs so the naming is not relevant and it is never executed.
I would start with checking who is presenter.
According to Apple docs on this:
When you present a view controller modally (either explicitly or implicitly) using the present(_:animated:completion:) method, the view controller that was presented has this property set to the view controller that presented it. If the view controller was not presented modally, but one of its ancestors was, this property contains the view controller that presented the ancestor. If neither the current view controller or any of its ancestors were presented modally, the value in this property is nil.
If the docs are correct then your presenter should be your "Viewcontroller with a tableview" which, I guess, is not UINavigationController. If that is the case then you should understand why your code fails.
It depends on your context of course, but I would just simplify a check this way:
var isPresentingInAddActionMode = self.presentingViewController != nil
... // your other code
if isPresentingInAddActionMode {
dismiss(animated: true, completion: nil)
}
else if let owningNavigationController = navigationController{
owningNavigationController.popViewController(animated: true)
}
If I understood your question and intent correctly then it doesn't matter for you who (which class) presented your detailVC and you care only about how your detailVC was presented - either pushed in navigation view controller or presented modally. I think just by checking presentingViewController property you can get that information.

Dismiss or remove previous modally presented view controller as soon as the next one appear modally

My target include a lot view need to present different view modally base on each user action. Here what I want to do to get cleaner view hierarchy and better user experience.
Root View Controller present First View Controller modally
When I clicked button on the First View Controller, then the Second View Controller appear modally over it.
As soon as the Second View Controller did appear, I want to dismiss or remove the first one from view hierarchy.
Can I do that? If so, how should i do it?
If not, what is the right way to solve this out cause I will present many modally presented view controllers over each view. I think even if I want to dismiss current view, the previous one will still remain appear when current one dismiss.
UPDATE :
VC1 (Root) > VC 2 (which was present modally) > VC 3 (which was
present modally over VC 2)
When i dismiss VC3, the VC2 is still on view memory. So, I don't want to appear VC2 as soon as I dismiss VC3 and instead I want to see VC1 by removing or dismissing VC2 from view hierarchy.
WANT : At the image, when I dismiss the blue,I don't want see the pink in my view memory and I want to remove it as soon as the blue one appear.
That's what i want to do.
Any Help?Thanks.
So, let's assume that you have a storyboard similar to:
What should happens is:
Presenting the the second ViewController (from the first ViewController).
Presenting the the third ViewController (from the second ViewController).
dismissing to the first ViewController (from the third ViewController).
In the third ViewController button's action:
#IBAction func tapped(_ sender: Any) {
presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil)
}
As you can see, by accessing the presentingViewController of the current ViewController, you can dismiss the previous hierarchy of the view controllers:
The view controller that presented this view controller.
By implementing presentingViewController?.presentingViewController? that means that: the presented of the presented current ViewController :)
It might seem a little bit confusing, but it is pretty simple.
So the output should be like (I added background colors to the viewControllers -as vc1: orange, vc2: black and vc3: light orange- to make it appears clearly):
EDIT:
If you are asking to remove the ViewController(s) in the middle (which in this example the second ViewController), dismiss(animated:completion:) does this automatically:
If you present several view controllers in succession, thus building a
stack of presented view controllers, calling this method on a view
controller lower in the stack dismisses its immediate child view
controller and all view controllers above that child on the stack.
When this happens, only the top-most view is dismissed in an animated
fashion; any intermediate view controllers are simply removed from the
stack. The top-most view is dismissed using its modal transition
style, which may differ from the styles used by other view controllers
lower in the stack.
Referring to what are you asking:
I think even if I want to dismiss current view, the previous one will
still remain appear when current one dismiss.
I think that appears clearly on the UI (and I find it ok), but as mentioned in the dismiss documentation discussion, both the third and the second will be removed from the stack. That's the right way.
Here is my opinion in different perspective,
Root View Controller present Second View Controller
Add FirstView onto Second View
Dismiss FirstView Controller when button pressed.
Second View Controller,
class ViewController: UIViewController, FirstViewControllerProtocol {
weak var firstViewController: FirstViewController?
override func viewDidLoad() {
super.viewDidLoad()
print("Not initiated: \(firstViewController)")
firstViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "FirstViewController") as? FirstViewController
addChildViewController(firstVC!)
firstViewController?.delegate = self
view.addSubview((firstViewController?.view)!)
print("Initiated: \(firstViewController)")
}
func dismiss() {
firstViewController?.view.removeFromSuperview()
firstViewController?.removeFromParentViewController()
}
}
FirstViewController,
protocol FirstViewControllerProtocol {
// Use protocol/delegate to communicate within two view controllers
func dismiss()
}
class FirstViewController: UIViewController {
var delegate: FirstViewControllerProtocol?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func dismiss(_ sender: Any) {
delegate?.dismiss()
}
deinit {
print("BYE")
}
}
What you want is an "unwind segue":
https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/UsingSegues.html#//apple_ref/doc/uid/TP40007457-CH15-SW8
https://developer.apple.com/library/archive/technotes/tn2298/_index.html
It allows you to dismiss multiple view controllers at the same time, without having to know how many there are in the stack.
In VC1 you would implement an IBAction called (for instance) unwindToRoot. Then in the storyboard for VC3, you wire up your Done button to the Exit object and choose the unwindToRoot action.
When that button is pressed, the system will dismiss all the view controllers it needs to bring you back to VC1.
This is better than calling presentingViewController?.presentingViewController?.dismiss(), because VC3 doesn't need to know anything about the view controller hierarchy underneath it.

How to dismass modal viewcontroller without navigationcontroller?

I have two UIViewControllers. There is no embedded navigationcontroller. They are connected by a segue, which is a "Present Modally".
I have a button on scene2. When it is clicked, I call this code:
self.navigationController?.popViewControllerAnimated(true)
I can see the code is hit but nothing happens. What am I doing wrong?
What you are doing wrong is that you are popping a view controller from a non existing navigation controller.
Basically when you say self.navigationController? that returns nil because there is no navigation controller. So the pop function doesn't get called.
What you have to do is call self.dismissViewControllerAnimated(true, completion: nil).
I think that you have to dismiss your controller and not pop it to back action:
self.dismissViewControllerAnimated(true, completion: {});

Dismiss multiple modal views with several navigation controllers

Try and follow me through the image below here. ViewController A is pushing navController A modally, which in turn shows ViewController A through ViewController F as show segues. The reason for this is that I need a UINavigationBar in order to go back and forth between the different UIViewControllers. But at one point in this interaction line you can actually push up another navigation interaction line, from ViewController B to navController B. They will both eventually end up at ViewController F, but with slightly different data. When I reach ViewController F and I'm all done with that UIViewController, I'd like to return to ViewController A instantly, without seeing multiple dismiss animations.
To do this I've used wide range of different lines of code, but they all end up with the same issue. I can get them to dismiss fine, so I end up at ViewController A. But when I've used the interaction line using navController B it will animate the dismissal of ViewController F, but there will still be a ViewController F behind that dismissal, and when the animation is done it flickers away and I end up at ViewController A. Basically it looks like there is another ViewController F behind the one I'm dismissing, but there isn't (I've checked in the 3D View in Xcode).
I can't seem to fix it. I tried removing navController B and it works fine, I only see 1 animation and end up at ViewController A without the issue explained above, but I have to have that navController B there. Simply put there seems to be an issue when using several UINavigationControllers with modal views.
Code to dismiss: ViewControllerA.dismissViewControllerAnimated(true, completion: nil)
I would try to avoid presenting modal view on a modal view. Consider pushing View Controller G from the View Controller B to the same navigation flow, rather then presenting it modally. This would allow you to always dismiss only one navigation flow of the NavController A.
What I finally ended up doing was to check wether the PresentingViewController at ViewController F was ViewController A or not since if you went through ViewController G the PresentingViewController would be ViewController B. And in ViewController F I first dismissed the front modal presentation without animation and then the second one with animation. It looks okay.
if let _ = presentingViewController as? ViewControllerA {
view.window?.rootViewController?.dismissViewControllerAnimated(true, completion: nil)
} else {
dismissViewControllerAnimated(false, completion: nil)
view.window?.rootViewController?.dismissViewControllerAnimated(true, completion: nil)
}
The view.window?.rootViewController?.dismissViewControllerAnimated(true, completion: nil) will start dismissing all the modals form the very rootViewController. Just adding two dismissViewControllerAnimated() after each other in the else-clause will not work.

Resources