ViewDidAppear on a UINavigationController not getting called when navigating back - ios

I have a UITabBarController that hosts 5 UINavigationControllers (let's call them N1 - N5). Each of the UINavigationControllers has UI elements that cause a UITableViewController to be pushed onto the navigation stack (I use MonoTouch.Dialog DialogViewController to implement these UITableViewControllers). Let's call those T1 - T5.
When I navigate between the tabs, the ViewDidAppear method gets called on each of N1 - N5 as expected. But when I touch a UI element on, say, N1, that causes T1 to get pushed onto the nav stack, and then try to go back using the back button, N1's ViewDidAppear method doesn't get called.
The funny thing is that if I "tab over" to a different tab (say N2) and then "tab back" to N1, the ViewDidAppear will be called as normal. And even if I have T1 pushed onto the nav stack, if I do the same tabbing around, N1's ViewDidAppear will still be called.
The MonoTouch code for N1 looks like this:
public class CalendarPage : UINavigationController
{
private DialogViewController dvc;
public override void ViewDidAppear (bool animated)
{
// initialize controls
var now = DateTime.Today;
var root = new RootElement("Calendar")
{
from it in App.ViewModel.Items
where it.Due != null && it.Due >= now
orderby it.Due ascending
group it by it.Due into g
select new Section (((DateTime) g.Key).ToString("d"))
{
from hs in g
select (Element) new StringElement (((DateTime) hs.Due).ToString("d"),
delegate
{
ItemPage itemPage = new ItemPage(this, hs);
itemPage.PushViewController();
})
{
Value = hs.Name
}
}
};
if (dvc == null)
{
// create and push the dialog view onto the nav stack
dvc = new DialogViewController(UITableViewStyle.Plain, root);
dvc.NavigationItem.HidesBackButton = true;
dvc.Title = NSBundle.MainBundle.LocalizedString ("Calendar", "Calendar");
this.PushViewController(dvc, false);
}
else
{
// refresh the dialog view controller with the new root
var oldroot = dvc.Root;
dvc.Root = root;
oldroot.Dispose();
dvc.ReloadData();
}
base.ViewDidAppear (animated);
}
}

I figured out what was going on. When the back button is pressed on the inner DialogViewController (created in ItemPage), the outer DialogViewController ("T1" above) is now the first responder, NOT the UINavigationController ("N1"). My confusion stemmed from the fact that I turned the back button off on that outer DialogViewController so I was assuming I had popped out all the way to the UINavigationController (N1), whereas I was still in a DialogViewController (T1).
I implemented the desired behavior (refreshing the contents of "T1") by creating a ViewDissapearing event on the inner DialogViewController (ItemPage in this case) and checking whether I am popping out - and if so, invoking the parent controller's ViewDidAppear method.
actionsViewController.ViewDissapearing += (sender, e) =>
{
if (actionsViewController.IsMovingFromParentViewController)
controller.ViewDidAppear(false);
};
Note the funny thing about this code is that the property that actually works is IsMovingFromParentViewController, NOT IsMovingToParentViewController (which is intuitively what you'd think would be set when you're navigating back). I imagine this may be a bug in MT.Dialog, but something that can't be fixed for back-compact reasons.
I hope this ends up helping someone...

Related

How to go back to the previous controller in one click after observing several controllers of the same type on iOS?

We are using MvvmCross 4.4.0 on the our iOS project and I faced the following problem:
I need to implement "Item" page with reference to the other
"Item" page;
I need an instant back navigation from any "Item" page to the previous controller ("Catalogue" controller).
Diagram:
Catalogue --ConcreteItem--> Item1 --MoreItems--> Item2 --MoreItems-->
Item3 --BackNavButton--> Catalogue.
I am doing the following thing in the Custom ViewPresenter:
var topViewController = ParentRootViewController.TopViewController;
ParentRootViewController.PushViewController(currentViewController, needAnimation);
if (topViewController.GetType() == currentView.GetType()
&& /*Logic to determine if its correct view types*/)
{
topViewController.RemoveFromParentViewController();
topViewController.Dispose();
}
And actually it works until I didn't return to the "Catalogue" page.
The problem is that I need to click back button so many times I had clicked "More" button on "Item" page. Also if we use custom back button with such code in both "Catalogue" and "Item" pages:
if (NavigationController?.NavigationBar?.BackItem != null)
{
var backbutton = new UIBarButtonItem(" ",
UIBarButtonItemStyle.Plain,
(sender, e) => { NavigationController?.PopViewController(true); })
{
Image = UIImage.FromBundle("BackButtonImage")
};
NavigationItem.LeftBarButtonItem = backbutton;
}
then app crashes when clicking "Back" NavButton on "Catalogue" page with in lambda
(sender, e) => { NavigationController?.PopViewController(true);
The disposed object ItemPageViewController.
The question is : How to correctly implement "SingleTop" page in MvvmCross?
Or
How to fix this problem?
P.S.: If from MvxPresenter remove line
topViewController.Dispose();
then in custom lambda would throw NullReferenceException.
P.P.S.: I believe it the problem that I don't remove controller from navigation stack. I have tried to remove controllers in Custom View Presenter, but, firstly, it is null there sometimes, and even with null check nothing helped.
UINavigationController has a function PopToViewController(UIViewController viewController, bool animated);
Instead of removing every ViewController when the views are of the same type, you could pop to the ViewController Catalogue when the backbutton is pressed.
UINavigationController has a property ViewControllers that we can use to find CatalogueViewController.
Since you're using MvvmCross we'll check for the ViewModel type.
var catalogueController = NavigationController.ViewControllers.First(c =>
((IMvxIosView)c).ViewModel.GetType() == typeof(CatalogueViewModel));
Now you can use the function PopToViewController to close all the views untill CatalogueController
CurrentNavigationController.PopToViewController(catalogueController, true);

Intercepting ios back button to send back to base view controller

We have a sequence of screens that navigate in order...
BaseViewController -> A -> B -> BaseViewController
Each screen uses NavigationController.PushViewController to go from...Base->A, A->B. So each subsequent screen is placed in the navigation stack.
So if you are on A and you click 'back', then it goes back one screen. This works well as it is controlled by the NavigationController.
However, when you are on screen B, 'back' should to back to the BaseViewController.
Instead it goes (as designed by Apple) back to A. Is there a way to intercept the 'back' button on B so we can instead use NavigationController.PopToViewController to send the user back to BaseViewController?
As #the4kman mentioned , we can create a custom button to replace the LeftBarButtonItem ,and handle the back event .
ViewDidLoad in B
this.NavigationItem.LeftBarButtonItem =
new UIBarButtonItem("back", UIBarButtonItemStyle.Plain, (sender,e) => {
UIViewController baseVC = NavigationController.ViewControllers[NavigationController.ViewControllers.Length - 3];
NavigationController.PopToViewController(baseVC, true);
});
As #J.C. Chaparro mentioned , remove A from stack .
ViewDidLoad in B
List<UIViewController> list = NavigationController.ViewControllers.ToList<UIViewController>();
list.RemoveAt(list.Count-2);
NavigationController.ViewControllers = list.ToArray();
You can do something like this in B's viewDidAppear function:
guard let navigationController = self.navigationController else {
return
}
navigationController.viewControllers.remove(at: navigationController.viewControllers.count - 2)
This will remove A from the stack, and allow you to go back to BaseViewController from B.
If I understand correctly you want to pop to root view controller from a certain top view controller. One way to do it would be to create a subclass of UINavigationController and override popViewController method where you would check what you have on top at the moment and decide to pop to root or not. Here's an example:
open class CustomNavigationController: UINavigationController {
override open func popViewController(animated: Bool) -> UIViewController? {
if topViewController is BViewController {
return popToRootViewController(animated: animated)?.last
} else {
return super.popViewController(animated: animated)
}
}
}
Approach:
Use child view controllers
Steps:
Create a view controller C which is a subclass of Base
Add A as a child view controller to C
Create a view controller D which is a subclass of Base
Add B as a child view controller to D
Push C to D
Thanks for all the answers. #Cole Xia put me on the right path for our scenario. His technique works, and the following works as well.
Here is the Xamarin code. The technique is to replace the current list of ViewControllers with a new one. Then when 'back' is hit on B, it goes right back to BaseViewController.
var viewControllers = new UIViewController[] { NavigationController.ViewControllers[0], new B() };
NavigationController.SetViewControllers(viewControllers, false);

Monotouch - Can't dismiss current view on button click

I have a view where I'm letting a user edit a EntryElement. I register the click event fine through the delegate of the "Done" button however, I can't get it to dismiss the current view to go to the previous view though. Here's what I'm trying right now:
AppDelegate.navigation.PresentedViewController.DismissViewController(true, null);
I've also tried:
AppDelegate.navigation.PresentedViewController.RemoveFromParentViewController();
navigation is just a UINavigationController
EDIT:
I am using Monotouch.Dialog to build out all of the views. This view is created via a method. I want it to go back to the previous view once they click the done button. Here's the contents of the method:
public static void EditClientTypeView (string typeName, string typeId)
{
DialogViewController editClientTypeVC;
UIBarButtonItem doneButton = new UIBarButtonItem (UIBarButtonSystemItem.Done);
EntryElement element = new EntryElement("Type Name", "Name of client type", typeName, false);
var root = new RootElement ("Edit Type") {
new Section () {
element
}
};
editClientTypeVC = new DialogViewController (root, true);
editClientTypeVC.NavigationItem.RightBarButtonItem = doneButton;
doneButton.Clicked += (sender, e) => {
// Need to save the edited client type to the database
Console.WriteLine("Done button clicked signifying the user is finished editing");
AppDelegate.navigation.PresentedViewController.RemoveFromParentViewController();
//AppDelegate.navigation.DismissViewController(true, null);
};
AppDelegate.navigation.PushViewController(editClientTypeVC, true);
}
Any help is appreciated!
Since you are PUSHING a view controller onto the Navigation Stack, you dismiss it by POPPING it off the stack. UINavigationController has three POP methods
PopToRootViewController - goes all the way back to the root view of your navigation controller
PopToViewController - pops back to a specific view controller
PopViewController - pops back to the "previous" view controller

HidesBottomBarWhenPushed leaves indicator

I have a settings dialog that I want to be full screen and cover the tab bar at the bottom of the screen. I used this SO answer and added HidesBottomBarWhenPushed to my view controller and it does hide the tab bar. Unfortunately it leaves behind the little triangle indicator subview that is displayed by the UITabBarController subclass.
I'm assuming there is some form of notification that I can subscribe to in order to hide the indicator but I don't know what that is. A little help here?
Maybe you could post a NSNotification when you set the bar to hidden using the method described here?
Can't access TabBarController from ImageView
I'm going to answer this myself because I think it's worth recording for future reference. I have a SettingsDialogViewController I wire up in the ViewDidLoad() method of my HomeDialogViewController:
NavigationItem.LeftBarButtonItem = new UIBarButtonItem("Settings", UIBarButtonItemStyle.Plain, (e, sender) => {
ActivateController (_settingsDvc());
});
The SettingsDialogViewController is created with HidesBottomBarWhenPushed = true. So when the settings dialog is activated, the bottom bar is hidden which causes the ViewWillLayoutSubviews() method of the CustomTabBarController to be called. By overriding that method I can set the visibility of my indicator based on whether the visible view controller (e.g. SettingsDialogViewController) hides the bottom bar when pushed. When that view controller is popped the indicator will automatically reappear.
public override void ViewWillLayoutSubviews () {
base.ViewWillLayoutSubviews ();
var selectedVc = SelectedViewController as UINavigationController;
indicator.Hidden = selectedVc != null && selectedVc.VisibleViewController.HidesBottomBarWhenPushed;
}
A final note, I found that the animation that occurred when activating the new settings view would display a black band across the screen below the status bar. I resolved this by setting the AutoResizingMask in the "from" view controller.
public override void ViewDidLoad () {
base.ViewDidLoad ();
NavigationItem.LeftBarButtonItem = new UIBarButtonItem("Settings", UIBarButtonItemStyle.Plain, (e, sender) => {
ActivateController (_settingsDvc());
});
View.AutoresizingMask = UIViewAutoresizing.FlexibleHeight;
}

Monotouch back to first ViewController

I'm having a problem going back again to my first Apps VC, the first "screen/ViewController)" is a login screen, then I call a UITabbar with their respective ViewControllers, when I'm on a certain level of a ViewController (for example the 5th one) I want to get a "Logout" behavior in my App and get back to the first ViewController (Login). But I think I can only Navigate back to the first ViewController of my Tabbar control. I was trying with this methods of my VC:
this.NavigationController.PopToViewController(previousVC,true);
or
this.NavigationController.PopToRootViewController(true);
Any help would be appreciated.
(If you need more details with the code please tell me about it)
//Call to the tab bar from the login viewcontroller
mainTBC = new TabBarMenuPpal ();
this.NavigationController.PushViewController (mainTBC, true);
…
//Tabbarmenuppal with a set of navcontrollers
public TabBarMenuPpal ()
{
var customerVC = new Customers ();
navCustomers = new UINavigationController ();
navCustomers.PushViewController (customerVC, true);
navCustomers.TopViewController.Title = customersTitle;
navCustomers.TabBarItem = new UITabBarItem (customersTitle, UIImage.FromFile ("Utility/Images/Cust-30x30.png"), 0);
…
mainVC = new UIViewController[]
{
navCustomers
, navSettings
} ;
this.ViewControllers = mainVC;
}
The problem is that when I start to navigate in the navCustomers I can't find a way to go back to the login VC (deleting my tab bar and releasing all their resources). (Sorry about my English).
I don't know if it's the best solution but I have implemented a "GoToLogin" method in my UITabBarController which only does this:
public void GoToLogin()
{
this.NavigationController.PopToRootViewController(true);
}
So when I need to go to my first page I only do this:
if(MainDialog.NavigationController.TabBarController != null)//loaded from the tab bar
((TabBarMenuPpal)MainDialog.NavigationController.TabBarController).GoToLogin();
else//loaded from the login screen
MainDialog.NavigationController.PopToRootViewController(true);

Resources