unwind delegate does not deinit a "middle" view - ios

I have a Navigation Controller (NC) and three ViewControllers (A, B, C) with the following navigation pattern:
NC->A->B->C
If I go from A to C and 'back' both B and C deinit method gets called.
If I use an Unwind Segue to go from C to A, B deinit does not get called.
Not clear why - I have another similar sequence in my App and deinits get called regardless of 'back' or 'unwind' operation.
Any idea what could cause B to 'stay alive' post an unwind operation?

Found the issue. The problem is this line I had in viewDidLoad:
definesPresentationContext = true
From the docs:
Determines which parent view controller's view should be presented over for presentations of type UIModalPresentationCurrentContext. If no ancestor view controller has this flag set, then the presenter will be the root view controller.
As a result, the middle view (i.e. 'B') becomes the root unless it is dismissed by a 'back' operation.
To avoid having the search box still visible for a split second on view C, I added searchController.active = false in prepareForSegue of view B after I obtain the chosen value from user selection (tap on row).

Related

Detect navigation pop in destination view controller

Using a UINavigationController in Xamarin.iOS (targetting iOS 11/12) I'd like to detect, in the destination View Controller when I'm being navigated back to - i.e. a pop. I specifically want to exclude/detect when the destination VC is being pushed for the first time.
To be explicit, given that we've navigated from A to C, and back to B:
[A] -(push)-> [B] -(push)-> [C] -(pop)-> [B]
I'd like to detect/discriminate - in B - between the initial push from A -> B and a subsequent pop from C -> B.
Conceptually this is identical to the questions posed here and here and I should - apparently - be able to use a combination of isMovingToParentViewController / IsBeingPresented / View.Window in ViewWillAppear() but, having tried the approaches in both the linked questions (and a few other things), I'm not seeing the expected values for these properties; they're always False, and View.Window is always null.
Is this a Xamarin.iOS-specific quirk, or am I missing something? If it is Xamarin-related is there a workaround? And if not does anyone have a bare-bones C# example of this detection working?
I'd prefer not to have to maintain state in the navigation controller (or delegate, or individual VCs) since the app structure could change in the future. However, a solution that made use of the nav controller or delegate to indicate the direction of navigation (pop vs push) to the destination VC - 'B', above - would be acceptable.
If you want to manipulate navigation operation, isMovingToParentViewController should work. Place it in your B controller's view will appear lifecycle event:
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
if (IsMovingToParentViewController)
{
// Come from A
}
else
{
// Pop from C
}
}
Here is the effect, I tested it on iOS 12:
Another way to detect it is:
When you want to push B from A, you have to initialize a new B for pushing so B's ViewDidLoad event will be called. When popping from C, B has existed in your navigation stack so that the ViewDidLoad won't be fired.

Should a view controller within a container view dismiss itself or its parent?

If a view controller A is modally presented and it contains a container view containing a view controller B.
Then if B wishes to dismiss itself (which in effect is also effectively dismissing A because B is contained within A) then should B call self.dismiss() or self.parent?.dismiss()?
Since B is a separate entity and B may not necessarily be contained in A, if B wishes to dismiss itself, it can call self.dismiss().
There will be 2 cases. If B is presented by itself, then it would dismiss as usual. If B is contained, then the OS would see that it's contained within A, then dismiss A.
However, if the only use of B is to be contained in A, then it makes sense to let A handle the navigation. The part where B "wishes" for itself to be dismissed can be done using delegates or blocks.

How can a PresentingViewController get notified that its PresentedViewController dismissed itself?

Given:
ViewController A that presents ViewController B
ViewController B has no reference to ViewController A (except implicitly the presentingViewController property)
ViewController B calls dismiss on itself and does nothing else
What I want to achieve:
ViewController A wants to know when ViewController B got dismissed in order to clean up some state
Restrictions:
I do not want to use KVO
I do not want to modify ViewController B or its behavior in any way
What I have found out so far:
dismiss(animated:completion:) according to the documentation forwards the call to its presentingViewController. But as it seems dismiss(animated:completion:) is not called, but rather a private method _performCoordinatedPresentOrDismiss:animated:.
iOS documentation on presentingViewController is misleading. It says "the view controller that was presented has this property set to the view controller that presented it", but that's not true. In iOS 11, this will always point to the root parent VC of the VC that present was called on. Similarly the documentation on presentedViewController is misleading. It says "the view controller that called the method has this property set to the view controller that it presented", that's not the whole story. Every VC in the hierarchy of the VC (all its parent VCs and child VCs) that called present will point to the same presentedViewController.
In your Controller A , kame it as UINavigationControllerDelegate and with navigationController:didShowViewController mark the presentation of Controller B (isControllerBisPresented = true). When viewDidAppear of B , check if isControllerBisPresented is true.
An ugly workaround would be to use a man-in-the-middle which does something in deinit. So A presents M which embeds B as a childVC. When B dismisses itself, M will be dismissed implicitly as well, so it's deinit method should be called. There it can notify A that it was dismissed.
This is fragile, as some reference cycle could prevent M from being deallocated which would result in A not getting notified. So I'd rather want to find a better solution.

Custom UIStoryboardSegue to unwind multiple view controllers

Assume that we have 3 view controllers: A, B, and C. A is the root view controller, which has presented B, and B has in turn presented C. How can we perform a custom unwind segue from C to A that does not reveal B?
A - B - C
A UIStoryboardSegue has a reference to its source view controller as well as its destination view controller, but what about the view controllers in between (B in our example)? We can perform whatever animations we want on C, but I don't see how we can affect B in any way.
The goal is simply to dismiss all but the root (B and C) to the right while having A come in from the left, so both source and destination are swiping horizontally next to each other. B should not be visible at any point of the animation.
My example dismisses only two views, but I hope to find a solution that would apply to an arbitrary number of views. Furthermore, I am not interested in solving this using a UINavigationController. I have tried simply dismissing B, which does indeed dismiss C as well, but you can still see both B and C during the animation.
You can dissmiss your controller as below :
self.presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil) // you need to track viewcontroller's stack hierarchy

iOS Storyboards: How to segue to the second view with a back button to the first view, but without displaying the first view

Here is my storyboard configuration:
Navigation Controller -> View Controller A -> Push-> View Controller B
^
|
Modal
^
|
View Controller C
What I want to achieve: When a button is pressed in View C, directly View B will be opened modally (No part of View A is to be displayed). Also, View B will have a navigation back button to View A.
To achieve this,
I set up the illustrated storyboard.
I created a segue between View C and the Navigation Controller of View A/B.
In the 'prepareForSegue' method of View Controller C, I get an instance of View Controller A as the first element in the navigation. In this instance, I set a variable like 'directlyProceedToViewB=YES'.
In the viewDidLoad method of View Controller A, I check the variable 'directlyProceedToViewB' and if it is YES, I call 'performSegueWithIdentifier' to segue to View B
The result is so that, first View A is opened modally and after displaying it a very short time, View B is opened with a push animation (View B can navigate back to View A, which is good). But I do not want View A to be displayed any time at all. How can I achieve this?
EDIT:
To better visualize, I'm adding a screenshot with more example cases to support:
Here are some cases I want to support:
We can start with ViewC, click on 'Modally Display B' which opens ViewB, then click 'Back to A' to navigate back to ViewA, then click on 'Dismiss Modal' on ViewA to go back to ViewC
We can start with ViewD, clcik on 'Modally Display A' which opens ViewA, then click on 'PushB' to open ViewB, then go back and forth between A and B and modally dismiss to ViewD.
First of all, some corrections: those are not views but view controllers. And "view A" is not pushed into the UINavigationController but it's the root.
After that, I suggest making the segue in "view C" an unwind segue and implement the IBAction in "view A" by pushing "view B" with [[self navigationController] pushViewController:bViewController animated:NO].
EDIT (adding some details):
I assume that in ViewControllerA's viewWillAppear you present ViewControllerC in a not animated manner.
Implement an unwinding action like (IBAction)unwindAndThenGoToB:(UIStoryboardSegue *)segue in ViewControllerA.
In the storyboard connect the button in ViewControllerC to the Exit icon and select the previously defined method.
Then implement the method with the push call I wrote earlier.
ps: for documentation there is plenty on Apple's website.
Implement this using delegates.Decalre protocol in which class you want and define those methods and call the methods in the view controller you want.There is no many ways of calling some view and showing back button to go different view.modal view is just a concept.and you can use delegate methods to call whatever class you want.
Here I got a way to do so:-
You need to set no animation for segue from viewC to viewA as shown in below image. Then set a segue identifier for segue from viewA to viewB namely, "viewB" and in your viewA .m file add following code,
- (void)viewDidLoad {
[super viewDidLoad];
// Place your conditional check here.
[self performSegueWithIdentifier:#"viewB" sender:self]; //Will directly lead to viewB and viewA won't be shown as no animation is there from viewC to viewA.
}
And your rest flow be like-wise.
I found the solution myself.
First, I discovered that, my original proposal of
In the viewDidLoad method of View Controller A, I check the variable
'directlyProceedToViewB' and if it is YES, I call
'performSegueWithIdentifier' to segue to View B
works as I desired on iOS 7 but does not work on iOS 8.
So the solution is, in the viewDidLoad method of View Controller A, if 'directlyProceedToViewB' is YES, rather than calling performSegueWithIdentifier, use the following code:
ViewControllerB *destVC = [self.storyboard instantiateViewControllerWithIdentifier:#"ViewControllerBStoryboardID"];
[self.navigationController pushViewController:destVC animated:NO];

Resources