I am making a simple game. The storyboard flow looks like:
[Title Screen] --> [Game Screen] --> [Game Over Screen]
From the Game Over screen, I'd like to jump back to the title screen directly, without seeing the Game Screen appear, when the user presses MENU.
I first attempted this in my GameOverViewController:
- (void) pressesBegan:(NSSet<UIPress*>*) presses withEvent:(UIPressesEvent*) event
{
for (UIPress* press in presses)
{
if (press.type == UIPressTypeMenu)
{
[self.navigationController popToRootViewControllerAnimated:NO];
return;
}
}
[super pressesBegan:presses withEvent:event];
}
It pops correctly, but the animation is not disabled. You can clearly see the game screen appear for a second, with a very visible fade in/out transition. The game view even gets -viewWillAppear: and -viewWillDisappear: messages.
So next, I tried replacing the pop an unwind segue.
[self performSegueWithIdentifier:#"Game Over to Title" sender:self];
This effect looks slightly different—the game screen only appears for an extremely brief duration for some reason—but it is visible for at least 1 or 2 frames before we get back to the title. I've disabled the "Animates" checkbox on the unwind segue in Xcode.
How should I go about this? I'm about ready to just dump storyboards entirely and try doing it another way. I feel like they're more complicated than managing the nibs myself. If I can't get the segues to go cleanly, the only other alternative I can think of is to introduce a dummy black screen into the hierarchy and figure out how to transition via a pop and a segue:
[Game Screen]
/
[Title Screen] --> [Dummy Black Screen]
\
[Game Over Screen]
Include all your "poppable" navigation in a new navigation controller, and pop the navigation controller itself whenever you need. I think it has to be modally presented but I'm unsure about that part. Give it a try in both cases, it's pretty much a one-liner.
Or (this isn't clean) you could first pop the parent controller without animation, that would be invisible to the user, and then popToRoot just like you did. With the intermediate view already popped, you'd be good, its just not really re-usable anywhere else and is bad practice.
Related
I have an app that has an initial login screen then when the user wants to sign up, they are presented with a registration form that is three view controllers presented modally. When the user completes the form on the third screen (by pressing a "Done" button), I want the user to be taken back to the initial login screen.
I have tried doing this in the third view controller:
[self dismissViewControllerAnimated:NO completion:nil]
[self.presentingViewController dismissViewControllerAnimated:NO completion:nil]
[self.presentingViewController.presentingViewController dismissViewControllerAnimated:NO completion:nil]
However it only dismissed two of the view controllers and not all 3. Why did this happen?
As other people pointed out, there are more elegant/efficient/easier ways to achieve similar results from the UX perspective: via a navigation controller, or a page view controller, or other container.
Short/quick answer: you need to go one step further in the chain of presenting view controllers, because the dismissal request needs to be sent to the controller that's presenting, and not to the one that's being presented. And you can send the dismiss request to that controller only, it will take care of popping from the stack the child controllers.
UIViewController *ctrl = self.presentingViewController.presentingViewController.presentingViewController;
[ctrl dismissViewControllerAnimated:NO completion:nil]
To explain why, and hopefully help other people better understand the controller presenting logic in iOS, below you can find are more details.
Let's start from Apple documentation on dismissViewControllerAnimated:completion:
Dismisses the view controller that was presented modally by the view controller.
The presenting view controller is responsible for dismissing the view controller it presented. If you call this method on the presented view controller itself, UIKit asks the presenting view controller to handle the dismissal.
Thus [self dismissViewControllerAnimated:NO completion:nil] simply forwarded the request to self.presentingViewController. Which means the first two lines had the same effect (actually the 2nd line did nothing as there was no presented controller after the 1st one executed).
This is why your dismissal of view controllers worked only the top 2 ones. You should've start with self.presentingViewController and go along the chain of presenting view controllers. But this is not very elegant and can cause problems if later on the hierarchy of view controllers changes.
Continuing to read on the documentation, we stumble upon this:
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.
So you needn't call dismissViewControllerAnimated:completion: three times, a call on the controller that you want to come back will suffice. At this point, passing a reference to that controller would be more reliable than navigating through the stack of view controllers.
There are some more useful details in the documentation, for example regarding what transitions apply when dismissing multiple controllers at once.
I recommend you go through the whole documentation, not only for this method, but for all methods/classes that you use in your application. You'll likely discover things that will make your life easier.
And if you don't have the time to read all Apple's documentation on UIKit, you can read it when you run into problems, like in this case with dismissViewControllerAnimated:completion: not working as you thought it would.
As a closing note, there are some more subtle issues with your approach, as the actual dismissal takes place in another runloop cycle, as it's possible to generate console warnings and not behave as expected. This is why further actions regarding presenting/dismissing other controllers should be done in the completion block, to give a change to UIKit to finish updating its internal state.
Totally understood. What I will do is embed a navigation controller instead of using modal. I have a case just like you. I have LoginViewController to be the root view controller of the UINavigationController. SignupViewController will be presented by push method. For ResetPasswordViewController, I will use modal because it's supposed to go back to LoginViewController no matter the results. Then, you can dismiss the whole UINavigationController from SignupViewController or LoginViewController.
Second approach will be like, you come up with your own mechanism to reference the presented UIViewController via a shared instance. Then, you can easily dismiss it. Be careful with the memory management. After dismissing it, you should consider whether you need to nil it right away.
I know three ways to dismiss several viewControllers:
Use a chain of completion blocks
~
UIViewController *theVC = self.presentingViewController;
UIViewController *theOtherVC = theVC.presentingViewController;
[self dismissViewControllerAnimated:NO
completion:^
{
[theVC dismissViewControllerAnimated:NO
completion:^
{
[theOtherVC dismissViewControllerAnimated:NO completion:nil];
}];
}];
Use 'viewWillAppear:' method of viewControllers
~
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (self.shouldDismiss)
{
CustomVC *theVC = (id)self.presentingViewController;
theVC.shouldDismiss = YES;
[self dismissViewControllerAnimated:NO completion:nil];
}
}
Pass a reference to LoginVC1 further down the chain.
(This is the best approach so far)
Imagine you have some StandardVC, which presented LoginVC1.
Then, LoginVC1 presented LoginVC2.
Then, LoginVC2 presented LoginVC3.
An easy way of doing what you want would be to call (from inside your LoginVC3.m file)
[myLoginVC1 dismissViewControllerAnimated:YES completion:nil];
In this case your LoginVC1 would lose its strong reference (from StandardVC), which means that both LoginVC2 and LoginVC3 would also be deallocated.
So, all you need to do is let your LoginVC3 know that LoginVC1 exists.
If you don't want to pass a reference of LoginVC1, you can use:
[self.presentingViewController.presentingViewController dismissViewControllerAnimated:NO completion:nil];
However, the above approaches are NOT the correct ways of doing what you want to do.
I would recommend you doing the following:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
if (!self.isUserLoggedIn)
{
[UIApplication sharedApplication].keyWindow.rootViewController = self.myLoginVC;
}
return YES;
}
Then, when user finished his login process, you can use
[UIApplication sharedApplication].keyWindow.rootViewController = self.myUsualStartVC;
Problem
I am having a rather big issue with the iOS7 keyboard appearance. I have a Searchbar on a UIViewController with TableView Delegation/Data Source setup (I am using the self.searchDisplayController delegates as well). I segue from this scene to a prototype tableview to show the results.
Here is the issue:
On first load I can see the keyboard being displayed when I tap into the text field of the UISearchBar. I can type and perform a search with the results being shown in the next scene.
I've added NSNotifications to view the keyboard properties in local methods keyboardWillShow and keyboardWasShown. I can see on the first scene appearance (after the view is completely loaded):
I segue to the result tableview at this point and when I navigate back and touch the text field, my keyboard shows up either fully or partially off-screen:
When I look at the keyboardWillShow notification at this point I can see that my keyboard values are incorrect:
I've researched and tried many possibilities including:
Added the following to my main view controller:
-(BOOL)canResignFirstResponder
{
return YES;
}
-(BOOL)canBecomeFirstResponder
{
return YES;
}
Configured the following in my view did load
self.searchDisplayController.searchBar.spellCheckingType = UITextSpellCheckingTypeNo;
self.searchDisplayController.searchBar.autocapitalizationType= UITextAutocapitalizationTypeNone;
self.searchDisplayController.searchBar.autocorrectionType = UITextAutocorrectionTypeNo;
self.searchDisplayController.searchBar.keyboardType = UIKeyboardTypeDefault;
Put in standard stubs for:
-(void)searchDisplayController:(UISearchDisplayController *)controller didShowSearchResultsTableView:(UITableView *)tableView
-(void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
I've noticed that if I choose a Partial Curl as my segue mode, the keyboard remains accessible when I roll back to the main view controller (but then it was never fully off screen in that case). However if I move from the results tableview to a detail scene and then navigate back to the main view controller, the keyboard appears off-screen again.
Question
Is there a method I can use to intercept the misplaced keyboard so that it displays in the default location?
NB: Along these lines, I have created a NSDictionary property to hold the initial userInfo values with the correct keyboard placement. I am not sure how to reassign these values to get the keyboard to return to it's original placement.
BTW - This seems a bit of a hack to get the keyboard fixed due to a bug in IB, is there some other way that I can try to remedy the situation?
Thanks in advance for any feedback!
Solution
This was such an obscure issue that I'm sharing the solution to save the next person some effort. Like most programming issues, it turns out this one was self-inflicted. In my original iteration of this project I had turned off rotational support as I am learning auto-layout and I wanted to ease into the transition from Springs and Struts. Somehow between the start of the project and the code release I ended up with this bit of code in the Main Scenes' View Controller.
//BAD
- (NSUInteger) supportedInterfaceOrientations
{
return !UIInterfaceOrientationPortraitUpsideDown;
}
instead of returning a valid enumeration like...
//OK
- (NSUInteger) supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskAll;
}
I've done a custom UITableViewCell` with a front view and a back view for doing something like mailbox.
I've added the gestureRecognizer to scroll the front view so
self.slideRecognizer =
[[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(slide:)];
[self.slideRecognizer setDelegate:self];
[self addGestureRecognizer:self.slideRecognizer];
The problem is that I cant scroll the UITableView down unless I tap outside the UITableViewCell (for example on UITableView header) and go down.
There should be something to add or edit in my method
- (void)slide:(UIPanGestureRecognizer *)panRecognizer
It's true?
THAT'S THE SOLUTION FOR ME ____________________________________________________________________________
Based on Simon's link, I've added this code in my tableViewController and in my custom cell
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:
(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
The problem was that assigning the custom pan gestureRecognizer I've invalidating the default one.
I would take a look at this question which seems quite similar. It sounds as though the gesture is only being captured by that cell and not passed through.
Tell ScrollView to Scroll after other pan gesture
I'm not sure if this is exactly the same issue as yours however. You should post more code.
On a side note, personally as a developer and a user I find scrolling elements inside scrolling elements to be every irritating to use. From a usability perspective I would consider a redesign. Thats only my opinion however.
I have an app that is controled through a UITabBar. In one of the sections within the tab bar, I have a navigation table. It works fine from an example I did from one of the books but I want to be able to go another view controller (aka another xib file) when the user selects a row and I want the user to be able to go back easily. I realize this has to do with pushingViewControllers but I am stuck. Here is where I think the problem is. my code is at the bottom. If you notice, I commented out
// [self presentModalViewController:flowerDetailViewController animated:YES];
While this did take me to the my flowerDetailViewController XIB file, I lost the ability to do navigation (go back). If anyone has any ideas or suggestions, it would be much appreciated.
Thank you
-(void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
FlowerDetailViewController *flowerDetailViewController =
[[FlowerDetailViewController alloc] initWithNibName:
#"FlowerDetailViewController" bundle:nil];
/*flowerDetailViewController.detailURL=
[[NSURL alloc] initWithString:
[[[flowerData objectAtIndex:indexPath.section] objectAtIndex:
indexPath.row] objectForKey:#"url"]];*/
flowerDetailViewController.title=
[[[flowerData objectAtIndex:indexPath.section] objectAtIndex:
indexPath.row] objectForKey:#"name"];
//[self presentModalViewController:flowerDetailViewController animated:YES];
[self.navigationController pushViewController:
flowerDetailViewController animated:YES];
[flowerDetailViewController release];
}
It doesn't make any sense to execute these two methods on the same view consecutively.
[self presentModalViewController:flowerDetailViewController animated:YES];
..
[self.navigationController pushViewController: flowerDetailViewController animated:YES];
because they give a very different result, from user experience point of view.
The first one presents a view modaly. Modaly means, that he application brings up the view to the top (imagine a Z-axis, when you hold the phone, it is a line from the phone to you, on top of it means closer to you), and the user is stuck in that view exclusively because it is on the top, he/she cannot touch anything else from the application unless he resolves the options presented in the view and the view goes away.
The second method is pushing the view onto he stack of views that all belong to the navigation controller. The navigation controller pushes views onto the screen like you would lay a stack of cards onto the table, card1, put onto that card 2, put onto that card 3...and so on card N. But you still have the ability to touch other options that are all around the navigation controller.To get back to the card 1, you need to remove card(views) that are on top of it, for removing on-top views, the navigation controller provides the back button automatically.
Only you cann tell, which of these two is handy in terms of your application UI and design.
I'm trying to add a split view inside of a tab bar, and since the split view isn't the root, it doesn't properly get the rotation notifications, so the delegate's methods are never called to add the button to the toolbar in the detail view.
I've rigged it up so I can generate the popover when rotated, but when this method is called, the view dissappears from the landscape mode, and if you activate it and then rotate back into landscape, it's a black empty box where the master view used to be. How do I get rid of this occuring?
-(void) displayPopover:(id)sender
{
//Toggle the popover: if it's showing, hide it
if (popoverController != nil && [popoverController isPopoverVisible])
{
[popoverController dismissPopoverAnimated:NO];
}
else
{
//Create a Popover displaying the master view
if (popoverController == nil)
{
popoverController=[[UIPopoverController alloc] initWithContentViewController:self->rootController];
popoverController.popoverContentSize=CGSizeMake(300, 500);
}
[popoverController presentPopoverFromBarButtonItem:[detailController.toolbar.items objectAtIndex:0] permittedArrowDirections:UIPopoverArrowDirectionAny animated:NO];
}
You will need to remove all the objects from window using:
[appdelegate window ] subviews] objectAtIndex:0] removeFromSuperview];
Then add your splitview to the window, you can get the view callbacks.
I would recommend either finding a way to get your SplitViewController to be root, or creating a custom subclass of the UISplitViewController that allows for non-root placement.
I really like what Matt Gemmell did here: http://mattgemmell.com/2010/07/31/mgsplitviewcontroller-for-ipad
Using a custom subclass like Matt's will allow you to benefit from all the same delegate methods that a SplitView as root would allow. I used it in a project where I wanted my SplitView to appear as a modal - almost impossible with a traditional UISplitViewController.
so your split view has rotation enabled (shouldAutorotateToInterfaceOrientation:) now you have to make sure that the tab controller has also rotation enabled (should be the appDelegate, am I right?) AND you have to make sure that every other view that is in your TabBar has also rotation enabled!
so if your TabBar contains 2 tabs you have to set the rotation in 3 classes.