How to prevent user from leaving view controller? - ios

I have an iPad app (XCode5, ARC, iOS7, Storyboards with a UITabBarController controlling the navigation). On one view, I have some required fields that I check for in -viewWillDisappear; if one of them is missing, I display an alert. The problem is I need to stay on that view until it's corrected. Unfortunately, the only place I can check for the required fields is in -viewWillDisappear.
Is there some way I can cause the view to complete the disappear and then go back to that same view? I have looked at SO and it doesn't appear to be a way, but I thought I'd ask anyway, just in case someone has figured out how to do it.. :D

You need to do
self.tabBarController.delegate = self
in your viewdidload and then implement the delegate method
- (BOOL)tabBarController:(UITabBarController *)tabBarController
shouldSelectViewController:(UIViewController *)viewController
{
if(conditions_satisfied)
return YES;
else
{
//show alert view here
return NO;
}
}
EDIT:It would appear that rdelmar was faster than me :)

You can set the delegate for the tab bar controller, and return NO from tabBarController:shouldSelectViewController: until whatever conditions you set are met.

Related

iOS: Best practice to know when two view controllers are loaded after navigation push

I have an iOS app that displays multiple screens and has different root controller for both iPhone and iPad. Here is a simplified example code to show what is going on.
if (iPad) {
self.sideMenuController = [LGSideMenuController sideMenuControllerWithRootViewController:viewControllerA
leftViewController:viewControllerB
rightViewController:nil];
} else {
self.sideMenuController = [LGSideMenuController sideMenuControllerWithRootViewController:viewControllerB
leftViewController:viewControllerA
rightViewController:nil];
}
[self.navigationController pushViewController:self.sideMenuController animated:NO];
I need to be able to tell when both controllers (ViewControllerA and ViewControllerB) is loaded.
I've implemented the following delegate
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (self.viewControllerA.viewIfLoaded.window != nil && self.viewControllerB.viewIfLoaded.window != nil) {
// do stuff after both controllers have been loaded and it is current view.
}
}
The delegate solution works, but not sure if it is best practice. I check if viewControllerA and viewControllerB is not nil and current view controllers because I push other controllers in the navigation controller and don't want to do anything if that happens.
It seems fragile. You're making a lot of assumptions and not (as far as I can tell) asking the navigation controller the question you really want to know the answer to. That question would be (as far as I can tell):
Is the view controller that just got pushed one of VCA and VCB?
Is the other of that pair already the navigation controller's child?
Assuming that the root view controller always loads first, you could perform work in the viewDidLoad method of the non-root view controller, which you could determine by referencing the device type.
You can have some BaseClass that both viewControllers inherit from (ViewControllerA and ViewControllerB) and in that BaseClass, you can use viewDidLoad: method to run whatever code you want.

UIControllerView Back swipe stops working when setting navigationController.interactivePopGestureRecognizer.delegate = self;

As title says. I had one controller where I was setting interactivePopGestureRecognizer.delegate to handle logic when to allow back swipe gesture and when not. It worked. But now I noticed that once I setup the delegate, the back swipe stops working. It really causes that one line of code. But why?
Yes, the controller where I used to handle the backswipe logic had everything needed (UIGestureRecognizerDelegate protocol, gestureRecognizerShouldBegin delegate method with return YES), but as I say, I discovered in another controller that by just calling the one line of the following code, back swipe doesn't work anymore. (Yes this another controller conforms to UIGestureRecognizerDelegate protocol)
self.navigationController.interactivePopGestureRecognizer.delegate = self;
It doesn't help if I add also:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
return YES;
}
or
self.navigationController.interactivePopGestureRecognizer.enabled = YES;
I wonder what is causing this? If I don't call that one line of code, back swipe works! And it even worked in the another controller where I handled the logic as I said.
Edit: I was setting the delegate from viewDidLoad. I tried also from viewDidAppear, but nothing.
The issue is because you are setting multiple ViewController as the delegate. Once you set any ViewController as the UIGestureRecognizerDelegate delegate, that ViewController is responsible for handling the gesture and previously set any delegate will be invalid
To fix the issue you can set the delegate again when view appears in viewWillAppear
self.navigationController.interactivePopGestureRecognizer.delegate = self;
For some reason, if I add the following code to controller, back swipe again works. Maybe it is because i have scrollview in the controller view, but it was working before even without the following code and then it stopped. Strange. (May be i didnt have tableview on the controller where it worked, i dont remember, but i was trying it even with hidden table when it stopped working)
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
As said here.

Set UINavigationBar title from view contained within UIPageViewContoller

Here is the problem I am having. I am unable to set the UINavigationBar title for the views I have contained within a UIPageViewController.
The basic architecture of the app is as follows.
The root view controller for the app is a UITabBarController, with 5 navigation controllers contained in it.
The first Navigation controller, which is the one I am having issues with, contains a page view controller and this page view controller contains a number of UIViewControllers.
I want that, when I swipe through each of these view controllers, I can set the title in the UINavigationBar.
I have tried the following:
In the UIViewController contained within the page view controller, I have tried [self setTitle:#"Title I want"] - it didn't work.
Within the same UIViewController I have also tried [self.navigationBar.navigationItem setTitle:#"Title I want"] - this also didn't work.
I also tried setting the title for the View controller and attempted to extract that inside the PageViewControllers delegate method transitionCompleted, but this didn't work either.
I am wondering should I go back to the drawing board, and whether I am going down a rabbit hole with this view layout architecture. Has anyone else encountered this issue and if so, how did you solve it?
Edit: I must also add that I am doing this programatically.
Thanks for the help.
So, in the end I came up with a way to get this working, albeit not the cleanest solution that I wanted, but suitable for the purpose nonetheless.
I created a new class called PageLeafViewController and set up its init method as below. Child view controllers of a page view controller inherit from this. Here is the code.
Code sample
- (id)initWithIndex:(NSUInteger)index andTitle:(NSString *)navBarTitle; {
if(self = [super init]) {
self.index = index;
self.navBarTitle = navBarTitle;
}
return self;
}
These can be initialised like so before being added to the UIPageViewController.
Code sample
ChildViewController *aChildViewController = [[ChildViewController alloc] initWithIndex:1 andTitle:#"A Title"];
You will need to add a UIPageViewControllerDelegate to your interface for your page view controller. This is so you can implement the code for the delegate methods for when your view transition has been completed, and you need to set the title.
When the UIPageViewController loads, I grab the first view controller and get its title, setting it to the UINavigationController navigation bar
Code sample
PageLeafViewController *initialViewController = (PageLeafViewController *)[self viewControllerAtIndex:0];
[self.navigationItem setTitle:initialViewController.navBarTitle];
When a transition occurs, we set the title again to that of the new child view controller, when the transitioning into view has completed.
Code sample
- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed {
PageLeafViewController *currentLeaf = (PageLeafViewController *)[self.pageViewController.viewControllers lastObject];
[self.navigationItem setTitle:currentLeaf.navBarTitle];
}
Note: The above gets called automatically when a new child view controller has been displayed.
While this is not the most elegant solution it works for now, and I don't think its possible to call a function from within a child view to update the NavigationBar title, unless someone wants to correct me?
Hope this helps.
I don't think you're supposed to set the title on the navigationBar, have you tried self.navigationController.title = #"Title"; ?

Impose a condition needing to be satisfied before user changes to another tab

I have situation where I need to make sure that user has completed certain steps before they move to another tab inside UITabBarController. So if the user is in middle of something and taps on another tab, I would like to show a UIAlertView saying "you must complete blah blah blah before you go to another tab."
Is it possible to to check this condition and cancel moving to another view controller?
Sure you can. I suppose you have your tabbar controller in the AppDelegate class. If so, set the AppDelegate to be its delegate. Then implement the following method
- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController {
// place all the checks here
EditingViewController *editingController = //link to controller where editing is being made.
if (editingController && editingController.isEditing) {
//UIAlertView
return NO;
}
return YES;
}
At a guess you could try catching the view on it's way out and changing the selected index on the tab bar controller to be the view you wish to keep them on:
- (void)viewWillDisappear:(BOOL)animated {
self.tabBarController.selectedIndex = 0;
}
You might find that's a bit jerky though depending on the order of events, a quick google has found that if you can make your view controller a UITabBarControllerDelegate then you can implement:
- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController
Which would allow you to catch them earlier. You might find it simplest to implement this in your App Delegate and have it know (or check) if it should allow the change away.

How do I hook into the action method for an iPad popover toolbar button?

I am using the split view template to create a simple split view that has, of course, a popover in Portrait mode. I'm using the default code generated by template that adds/removes the toolbar item and sets the popover controller and removes it. These two methods are splitViewController:willShowViewController:... and splitViewController:willHideViewController:...
I'm trying to figure out how to make the popover disappear if the user taps on the toolbar button while the popover is displayed. You can make the popover disappear without selecting an item if you tap anywhere outside the popover, but I would also like to make it disappear if the user taps the button again.
Where I'm stuck is this: there doesn't seem to be an obvious, easy way to hook into the action for the toolbar button. I can tell, using the debugger, that the action that's being called on the button is showMasterInPopover. And I am new to working with selectors programmatically, I admit.
Can I somehow write an action and set it on the toolbar item without overriding the action that's already there? e.g. add an action that calls the one that's there now? Or would I have to write an action that shows/hides the popover myself (behavior that's being done behind the scenes presumably by the split view controller now???).
Or am I missing an easy way to add this behavior to this button without changing the existing behavior that's being set up for me?
Thank you!
So it turns out that you can make the popover dismiss when clicking on the barButtonItem by implementing the SplitViewController willPresentViewController method as follows:
- (void) splitViewController:(UISplitViewController *)svc
popoverController: (UIPopoverController *)pc
willPresentViewController: (UIViewController *)aViewController
{
if (pc != nil) {
[pc dismissPopoverAnimated:YES];
}
}
So, the barButtonItem will have the UISplitViewController as the target and showMasterInPopover: as the action. I can't find it in the documentation, so I'm a bit worried it's not okay to call it, but I got it to work by changing the target to self (the view controller) and the action to a custom method, like this:
- (void)showMasterInPopover:(id)sender {
// ...insert custom stuff here...
[splitViewController showMasterInPopover:sender];
}
Don't have the rep to make a real comment. :-(
#Jann - I'm pretty sure what Elizabeth wants to do is pretty standard. For example, the Notes application that ships pre-loaded on the iPad closes and opens the popover when you press the toolbar button in the top left corner.
Below is my solution. It starts out similar to greenisus' solution, by hooking the UISplitViewController's toolbar button event handler. I use a flag in my controller to track whether the popover is open or not. Finally, to handle the case where the user opens the popover, then closes it by clicking outside the popover, I implement the UIPopoverControllerDelegate protocol.
First, the controller interface:
#interface LaunchScene : NSObject <UISplitViewControllerDelegate, UIPopoverControllerDelegate>
{
UISplitViewController* _splitViewController; //Shows list UITableView on the left, and details on the right
UIToolbar* _toolbar; //Toolbar for the button that will show the popover, when in portrait orientation
SEL _svcAction; //The action from the toolbar
id _svcTarget; //The target object from the toolbar
UIPopoverController* _popover; //The popover that might need to be dismissed
BOOL _popoverShowing; //Whether the popover is currently showing or not
}
-(void) svcToolbarClicked: (id)sender;
I use _svcAction and _svcTarget to addess greenisus' worries that he might not be calling the right function.
Below is my implementation. For brevity, I have omitted the code that instantiates the UISplitViewController and the subviews. All the show/hide related code is shown.
//the master view controller will be hidden so hook the popover
- (void)splitViewController:(UISplitViewController*)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem*)barButtonItem forPopoverController:(UIPopoverController*)pc
{
_popoverShowing = FALSE;
if(_toolbar == nil)
{
//set title of master button
barButtonItem.title = #"Title goes here";
//Impose my selector in between the SVController, and the SVController's default implementation
_svcTarget = barButtonItem.target;
_svcAction = barButtonItem.action;
barButtonItem.target = self;
barButtonItem.action = #selector(svcToolbarClicked:);
//create a toolbar
_toolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, 1024, 44)];
[_toolbar setItems:[NSArray arrayWithObject:barButtonItem] animated:YES];
}
//add the toolbar to the details view (the second controller in the splitViewControllers array)
UIViewController* temp = [_splitViewController.viewControllers objectAtIndex:1];
[temp.view addSubview:_toolbar];
}
Here is my function, that responds to the toolbar click. This handles the case where the user taps and re-taps the toolbar button.
-(void) svcToolbarClicked: (id)sender
{
if(_popoverShowing)
{
[_popover dismissPopoverAnimated:TRUE];
}
else
{
//Perform the default SVController implementation
[_svcTarget performSelector:_svcAction];
}
//Toggle the flag
_popoverShowing = !_popoverShowing;
}
Some functions from UISplitViewControllerDelegate
//the master view (non-popover) will be shown again (meaning it is going to landscape orientation)
- (void)splitViewController:(UISplitViewController*)svc willShowViewController:(UIViewController *)aViewController invalidatingBarButtonItem:(UIBarButtonItem *)button
{
//remove the toolbar
[_toolbar removeFromSuperview];
}
// the master view controller will be displayed in a popover (i.e. the button has been pressed, and the popover is about to be displayed.
//Unfortunately triggers when the popover is ALREADY displayed.
- (void)splitViewController:(UISplitViewController*)svc popoverController:(UIPopoverController*)pc willPresentViewController:(UIViewController *)aViewController
{
_popover = pc; //Grab the popover object
_popover.delegate = self;
}
The above code is sufficient for most cases. However, if the user opens the popover, then dismisses by clicking elsewhere on the screen, the _popoverShowing boolean will contain an incorrect value, which will force the user to tap the toolbar button twice to re-open the popover. To fix this, implement the UIPopoverControllerDelegate method, like the snippet below.
//UIPopoverControllerDelegate method
- (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController
{
_popoverShowing = FALSE;
_popover = nil;
}
This took me forever to figure out, digging through the docs and (I think) most of the UISplitViewController questions on StackOverflow. I hope somebody finds it useful. If so, I covet reputation points. ;-)
Maybe you all just complicate it too much or I have read something very different than you guys wanted to do... but perhaps, this is what you were all trying to figure out so hardish:
-(void)togglePopOverController {
if ([popOverController isPopoverVisible]) {
[popOverController dismissPopoverAnimated:YES];
} else {
[popOverController presentPopoverFromBarButtonItem:bbiOpenPopOver permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
}
Elisabeth writes:
You can make the popover disappear without selecting an item if you tap anywhere outside the popover, but I would also like to make it disappear if the user taps the button again.
First of all, let me say that none of what I am about to say is to be taken personally -- it is not meant that way. It all comes from years of designing programming interfaces and studying the Apple Human Interface Guidelines (as well as having a Graphic Designer who is contstantly trying to teach me the right way to do things). It is meant as an opposing viewpoint and not as a rant.
What you are suggesting is a problem UI-wise for me, and will be an issue that causes trouble when Apple reviews the app. You are never supposed to have a known-UI-object perform a function that it does not perform normally (For instance: a button never shows and then releases a view/object/window. Toggles do this).
For instance, a magnifying glass on the navbar means Search (as defined by Apple). They have in the past, and will continue in the future to, refuse apps that use this for zooming the interface. For example: Apple Rejects ConvertBot or The Odyssey: Trail of Tears (search the page for it). The language in the rejection is always the same (bold marking what they would cite for your usage):
“… uses standard iPhone/iPod screen images in a non-standard way, potentially resulting in user confusion. Changing the behavior of standard iPhone graphics, actions, and images, or simulating failures of those graphics, actions, or images is a violation of the iPhone Developer Program agreement which requires applications to abide by the Human Interface Guidelines.”
Also, if you really want this feature, ask yourself: "Why?". If it is because you, yourself, like it, then I would really skip it. Most users would be confused by this behavior and would not actually use it because they would not know it was an option to use. Apple spent the last 3 years training iPhoneOS users how to use their OS and interface elements. The last thing you, as a programmer or designer, want to do is spend time trying to train a user on how to use your app. They will generally remove your app from their device and move to another similar app instead of forcing themselves to learn your way of doing things.
Just my $.02

Resources