Modal Segue Chain - ios

I have an iOS app that has a log in view (LognnViewController) and once a user is successfully authenticated they are taken to another view (DetailEntryViewController) to enter some simple details.
Once the details are entered the user is taken to the main part of the app that consists of a tab controller (TabViewController) that holds a variety of other views. The LogInViewController performs a modal segue to the DetailEntryViewController and the DetailEntryViewController then performs a modal segue to the TabViewController so I have kind of a modal segue chain going to get into the app. When a user logs out I want to go all the way back to the LogInViewController but when I do a:
[self.presentingViewController dismissModalViewControllerAnimated:YES];
...it pops the TabViewController and I end up back at the DetailEntryViewController instead of the first LogInViewController. Is there any way I can pop back to the first view controller easily or does doing this modal segue chain thing prevent me from that. I got the bright idea to put some code in the DetailEntryViewController viewWillAppear: that would automagically pop itself if the user had logged out but apparent making calls to dismiss a modal controller are not allowed in viewWillAppear: viewDidLoad:, etc.
Any ideas on how to make this happen?

I think this is not the best structure to implement your app. Modal controllers are supposed to be for temporary interruptions to the flow of the program, so using a modal to get to your main content is not ideal. The way I would do this is to make your tab bar controller the root view controller of the window, and then in the first tab's controller, present the login controller modally from the viewDidAppear method, so it will appear right away (you will briefly see the first tab's view unless you uncheck the "animates" box in the segue's attributes inspector). Present the details controller from that one, and then dismiss both modal controllers to get back to your main content. When the user logs out, just present that login controller again. I implement this idea like this. In the first tab's view controller:
- (void)viewDidLoad {
[super viewDidLoad];
_appStarting = YES;
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (_appStarting) {
[self performSegueWithIdentifier:#"Login" sender:self];
_appStarting = NO;
}
}
Then in the last (second in your case) modal view controller, I have a button method:
-(IBAction)goBackToMain:(id)sender {
[self.view.window.rootViewController dismissViewControllerAnimated:YES completion:nil];
}

Figured it out myself...just had to go up one more level to get to the "root" view controller (LogInViewController) and found that this did the trick:
[[self.presentingViewController presentingViewController] dismissViewControllerAnimated:YES completion:nil];
As I said I'm just getting the presentingViewController (DetailEntryViewController) and then going up one more level and getting that controller's presenter (LogInViewController).

I had similar problem and my "modal segue chain" was not limited. I agree with the arguments in the answer and comments below about modal segues designed for different thing, but I liked the "horizontal flip" animation of modal segues and I couldn't find the easier way to replicate them... Also in general I don't see anything wrong in using things that were designed for one thing to achieve some other thing, like chaining modal controllers. Repeated "partial curl" animation can also apply to some scenario in some app.
So I implemented the stack of modal controllers as a property of controller:
#interface ModalViewController : UIViewController
#property (nonatomic, retain) NSMutableArray *modalControllers;
#end
When the first modal segue is executed the stack is created in prepareForSegue method of controller that is not modal:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"modalSegue"]) {
ModalViewController *controller =
(ModalViewController *)[segue destinationViewController];
controller.modalControllers = [NSMutableArray arrayWithObject: controller];
}
}
When one modal controller moves to another the destination is added to the stack (in the method of ModalViewCotroller)
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"modalSegue"]) {
ModalViewController *destController =
(ModalViewController *)[segue destinationViewController];
// add destination controller to stack
destController.modalControllers = _modalControllers;
[destController.modalControllers addObject: destController];
}
}
To dismiss the whole stack at once was the most tricky part - you can't dismiss the previous controller before the next finished dismissing, so the cycle did not work, only recursive blocks did the trick, with avoiding the memory leak being the trickiest (I'm yet to check it, but I relied on this):
- (IBAction)dismissAllModalControllers: (id)sender
{
// recursive block that dismisses one auth controller
// all these dances are to avoid leaks with ARC
typedef void (^voidBlockType)();
__block void (^dismissController) ();
voidBlockType __weak dismissCopy = ^void(void) {
dismissController();
};
dismissController = ^void(void) {
int count = [_modalControllers count];
if (count > 0) {
// get last controller
UIViewController *controller =
(UIViewController *)[_modalControllers lastObject];
// remove last controller
[_modalControllers removeLastObject];
// dismiss last controller
[controller
// the first controller in chain is dismissed with animation
dismissViewControllerAnimated: count == 1 ? YES : NO
// on completion call the block that calls this block recursively
completion: dismissCopy];
}
};
// this call dismisses all modal controllers
dismissController();
}

[self.navigationController popToRootViewControllerAnimated:YES];

Related

XCode: Memory Leak When Performing Modal Segue

When a logged in user opens my application, they are sent to the main TabBarController from my AppDelegate, like so:
UITabBarController *tabBar = (UITabBarController *)self.window.rootViewController;
tabBar.selectedIndex = 2;
// (this is MainViewController in the tab bar)
Now, the user is in MainViewController. When the user selects a particular chat they'd like to enter, they are sent to the ChatViewController (not on the TabBarController), like so:
[self performSegueWithIdentifier:#"showChatSeg" sender:self];
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if([segue.destinationViewController isKindOfClass:ChatViewController.class]){
ChatViewController *destinationViewController = (ChatViewController *)segue.destinationViewController;
if(self.createdDialog != nil){
destinationViewController.dialog = self.createdDialog;
self.createdDialog = nil;
}else{
QBChatDialog *dialog = [ChatService shared].dialogs[self.selectedChat];
destinationViewController.dialog = dialog;
}
}
}
When this happens, I see a spike in memory usage, which makes sense. However, when the user leaves the ChatViewController and return to the MainViewController, like so:
- (IBAction)backButton:(id)sender {
[self performSegueWithIdentifier:#"fromChatToDashSeg" sender:nil];
// This is a storyboard segue back to the MainTabBarController
}
I get the following warning:
Attempt to present <MainTabBarController: 0x17ef28d0> on <ChatViewController: 0x17d6c940> whose view is not in the window hierarchy!
And the memory usage remains the same. And when the user enters a chat again, the memory continues to increase. Am I not dismissing the sending view controllers properly?
What you are doing is not going "back" but rather, you are presenting a copy of the previous view on top of the one you already have. That's why memory is building up, because you just keep stacking more and more views on top of eachother. Assuming you are using a modal segue to present your chat view, try calling:
[self dismissViewControllerAnimated:YES completion:nil];
It's because you're trying to perform a segue just to get back to your original location. All you need to do is dismiss your current modal view controller by calling [self dismissViewControllerAnimated:YES completion:nil];. Whenever you add a modal view to the view stack you want to call this method to exit, unless your intent is to add yet another view on top of the modal.

Switch to a view controller in a different tab after user performs action

I have the following storyboard in an application I am working on:
At the root, I have a Tab Bar Controller. It links to two View Controllers.
The first View Controller to display a newsfeed with pictures uploaded by the user (the one at the bottom in the storyboard).
The second View Controller serves to initiate the taking of a picture and attach some data to it. In the last step (top right), when touching "Save" in the right item of the Navigation bar, I want the user to be redirected to the newsfeed View Controller passing it some data.
I tried using a segue and it works. The data are passed to the newsfeed but the wrong tab is selected. I changed the selected tab using
[self.tabBarController setSelectedIndex:0];
But by tapping on the second tab again, things are messed up. I can see the newsfeed instead of the taking a picture screen. If I tap again, it crashes.
At some point I thought I may have got the wrong storyboard and should have implemented a TabBar in my newsfeed and handle the taking picture as a modal view.
Would you know any clean way to achieve this?
Thanks
You should not use a normal segue, which adds the destination controller to the stack. To do what you are trying to the best way should be to use an unwind segue. This is a rough sketch of what you need to do:
• Declare an unwind segue action in the NewsfeedController like (IBAction)unwindFromPictureSaved:(UIStoryboardSegue *)segue;
• Connect your "Save" button in your SavingPictureController to the "Exit" icon in the storyboard and select the previously defined method;
• In the newly created unwind segue define its identifier with something like SavedPictureSegue;
• Define the data to be passed in SavingPictureController's header with something like #property (strong, readonly, nonatomic) id passedData;
• In SavingPictureController implement
-(void)prepareForSegue:(UIStoryboardSegue *)segue
{
if ([segue.identifier isEqualToString:#"SavedPictureSegue"]) {
_passedData = // Your data here
}
}
• In NewsfeedController now implement the previously defined method and fetch the data from (SavingPictureController *)segue.sourceController. Be sure to #import "SavingPictureController.h".
Thanks to #Davide, I created a subclass of TabBarController and implemented the method below:
// Find the appropriate controller to answer to an unwind segue
// For each child view controller
// Checks if it is a Navigation Controller
// If it is check its children view controllers
// Return the first view controller that answers the unwind segue
// This because I assumed the default behavior is just to check one level up (in this case, it would have stopped at the NavigationController)
// Based on https://developer.apple.com/library/ios/technotes/tn2298/_index.html#//apple_ref/doc/uid/DTS40013591-CH1-CCVC-SELECTING_A_CHILD_VIEW_CONTROLLER_TO_HANDLE_AN_UNWIND_ACTION
- (UIViewController *)viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender {
BOOL resChildren, res;
for(UIViewController *controller in self.childViewControllers) {
if ([controller isKindOfClass:[UINavigationController class]]) {
for (UIViewController *childController in controller.childViewControllers) {
resChildren = [childController canPerformUnwindSegueAction:action fromViewController:fromViewController withSender:sender];
if (resChildren) {
return childController;
}
}
}
res = [controller canPerformUnwindSegueAction:action fromViewController:fromViewController withSender:sender];
if (res) {
return controller;
}
}
return nil;
}
Then in the unwind method of the 'NewsFeedController" it is necessary to set the correct index to see the controller with something like:
[self.tabBarController setSelectedIndex:1];
I uploaded a demo on github at https://github.com/kintso/unwindSegueWithTabBarControllerAndNavigationController

Objective C: Back button segue not working

I have a simple app with 2 screens.
When I press a button to go from the first to the second, everything is performed successfully (including animation). However, when I click the back button on the second screen, I get the following warning:
Warning: Attempt to present <getTextViewController: 0x8f6aa30> on <SecondViewController: 0x946cc80> whose view is not in the window hierarchy!
EDIT: Please don't refer me to other questions regarding above warning - I already saw those, and they refer to other issues.
However, it still switches back to the first screen. Yet, the animation of the segue does not perform.
Also: Information (such as inputted text) in the first screen remains when I return to the first screen, while information in the second screen resets every time the screen comes up.
Here is how I call both operations:
Segue from View 1 to View 2:
Name: F21, Style: Modal, Transition: Cross Dissolve, Animation: True.
Segue from View 2 to View 1:
Name: F12, Style: Modal, Transition: Cross Dissolve, Animation: True.
Code in getTextViewController.m (View 1):
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if([[segue identifier] isEqualToString:#"F21"]){
UIViewController *v = [segue destinationViewController];
[self dismissViewControllerAnimated:NO completion:nil];
v = self;
}
}
-(void)performSegue:(NSString*)str{
[self performSegueWithIdentifier:str sender:self];
}
//In some other method:
[self performSegue:#"F21"];
Code in SecondViewController.m (View 2):
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if([[segue identifier] isEqualToString:#"F12"]){
UIViewController *v = [segue destinationViewController];
[self dismissViewControllerAnimated:NO completion:nil];
v = self;
}
}
-(void)performSegue:(NSString*)str{
[self performSegueWithIdentifier:str sender:self];
}
- (IBAction)goBack:(id)sender {
[self performSegue:#"F12"];
}
I would very much appreciate any help to understand why the first segue works while the second doesn't.
Thank you,
Dean
NOTE: Here is the full project - https://github.com/dean13-meet/firstIOSApp
EDIT: Updated git.
Im not exactly sure what you're trying to do in your prepareForSegue, their is no need to be dismissing VC's there. If you want to have a simple app where you go from VC1 to VC2 and then back again, your best bet is to use a segue and an unwindSegue.
So in your storyboard control drag from a button on VC1 to VC2 and select your segue type. Then in VC1.m setup the unwind segue such as:
- (IBAction)unwindFromViewController:(UIStoryboardSegue *)segue
{
//empty implementation
}
Finally, in your VC2 control drag from the back button to the green exit icon on VC2 and select your unwindFromViewController method.
That should do what you're looking for.
For the sake of simplicity, I would suggest using a push segue opposed to modal because it takes care of all the back buttons for you. If you don't like the idea of a navigation controller however, try dismissing the view with the following: Moving back from a Controller to a previous one

Clarification on prepareforsegue and presentModalViewController while using Storyboard

I have an app with a LoginViewController as the initial view.
Note: So in appDelegate.m, self.window.rootViewController is NOT the TabBarController.
After Auth, I present the main part of the app, which has a tabbarController (identifier:tabBar) with two tabs and one tab has a navigation controller. I am using Core Data, so I need to pass MOC.
If I use,
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
UITabBarController *obj=[storyboard instantiateViewControllerWithIdentifier:#"tabBar"];
[obj setSelectedIndex:0];// Which tab to show first
[self presentModalViewController:obj animated:YES];
It works good visually. Now I need to pass the MOC. Read about PrepareToSegue method,created a segue (modal, Not shown in pic) from loginVC to my TargetViewController (TabBar>NavigationController1>View1), named the segue "LoginSegue" and used the following code:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
UINavigationController *navController = (UINavigationController *)[segue destinationViewController];
View1 *devicelist = (View1 *)[[navController viewControllers] lastObject];
devicelist.managedObjectContext = managedObjectContext;
}
The TabBar does not show. How do i set the tabbar controller in this case?
I have been trying to get a grasp on getting a reference for the Modal Tabbar, but still not clear. Can some one explain in layman terms how to handle a situation like this?
I think it would be better to use a design that doesn't use a modal transition to the tab bar controller. Modal presentations are generally supposed to be for interruptions to the normal flow of the app, not for getting your main controller on the screen. There are two alternatives, that I think are better. You can leave the login controller as the initial root view controller of the window, but then switch it out for the tab bar controller (which will be the new root view controller of the window, and the login controller will be deallocated). This usually works ok, but I think in this case where you want to pass the MOC from the app delegate (I presume) to a controller in the tab bar controller, I think a second way would be better.
The second way to do this, and the way I usually do login controllers, is to have the tab bar controller be the root view controller of the window, and then present the login controller modally from the viewDidAppear method of the initial view (which would be the one you're calling View1). If you do this presentation with animation set to NO, the login controller will be the first thing the user sees:
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
static int first = 1;
if (first) {
LoginViewController *login = [self.storyboard instantiateViewControllerWithIdentifier:#"Login"];
[self presentViewController:login animated:NO completion:nil];
first = 0;
}
}
The if statement is in there so the presentation doesn't happen again when you come back from the login controller (you could do something more sophisticated like having a delegate call back to View1 from the login controller indicating that the login was successful if you want, but this works).
If the login succeeds, you just dismiss the login controller, and you'll be there in your first view (if it fails, you just never dismiss it, and maybe put up a message saying the login failed).
If you go this route, then you can pass the MOC in the app delegate like this:
UINavigationController *nav = [(UITabBarController *)self.window.rootViewController viewControllers][0];
View1 *devicelist = (View1 *)nav.viewControllers.lastObject;
devicelist.managedObjectContext = managedObjectContext;

Dismiss modal view then perform segue to open a second modal view

I have a HomeController that allows a user to log in and register. If the user clicks login, I open a modal view using a segue.
Inside the modal view there is a button that says register. The desired action is to close the login modal view and then open the registration modal view using performSegueWithIdentifier:
- (void)loginControllerDidRegister:(LoginController *)controller sender:(id)sender
{
NSLog(#"loginControllerDidRegister");
[self dismissViewControllerAnimated:YES completion:nil];
[self performSegueWithIdentifier:#"RegistrationSegue" sender:sender];
}
This correctly dismisses the modal view, then it calls performSegueWithIdentifier:, where I have logging code that shows it is being called just as if I had pressed the register button.
I think the animation of the login modal view disappearing may be interfering with the display of the second modal view. Any ideas on what can be done to fix this?
well you need to have your "second modal" vc initiated. this is what the "prepareForSegue:" method does. also you need to override the "perform:" method. this is going to be a bit more complicated than you think. if is helps here is a breakdown of how a segue works...
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender;
gets called and passes in "segue". behind the scenes
- (id)initWithIdentifier:(NSString *)identifier source:(UIViewController *)source destination:(UIViewController *)source;
gets called, and this is where "segue" is created.
the "segue" object has properties of
(NSString *)identifier
(UIViewController *)sourceViewController
(UIViewController *)destinationViewController
with out these a segue can not be performed. these are akin to manually allocating your view controller
SomeViewController *secondView = [SomeViewController alloc] initwithNibName:#"SomeViewController" bundle:nil];
then
[[segue destinationViewController] setModalTransitionStyle:UIModalTransitionStyle(...)];
which is...
secondView.modalTransitionStyle = UIModalTransitionStyle(...);
the (...) would be the "segue" transition selected in the storyboard.
lastly
[[segue sourceViewController] presentModalViewController:destinationViewController animated:YES];
which is just
[self presentModelViewController:secondView animated:YES];
is what makes it all happen. you will basically have to tweak with those under the hood goings on to get what you want working, but it is do-able.
you must put the performSegue of your second modal view controller in the completion block of the dismissViewControllerAnimated call. UINavigationController can't handle the presentation when it is still presenting the other modal view controller.
If someone has the same question.
- (void)loginControllerDidRegister:(LoginController *)controller sender:(id)sender
{
NSLog(#"loginControllerDidRegister");
[self dismissViewControllerAnimated:YES completion:^{
[self performSegueWithIdentifier:#"RegistrationSegue" sender:sender];
}];
}

Resources