XCode: Memory Leak When Performing Modal Segue - ios

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.

Related

EXC_BAD_ACCESS crash upon dismissing UIViewController

I have two buttons triggering segues to two different UIViewCOntrollers, using this code:
- (IBAction)newTransButton:(UIButton *)sender
{
[self performSegueWithIdentifier:#"newTransSegue" sender:self];
}
- (IBAction)switchAccountButton:(UIButton *)sender
{
[self performSegueWithIdentifier:#"selectAccountSegue" sender:self];
}
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
NSManagedObjectContext *localContext = [NSManagedObjectContext MR_contextForCurrentThread];
if ([[segue identifier] isEqualToString:#"newTransSegue"])
{
UINavigationController *navController = (UINavigationController *)segue.destinationViewController;
AddTransactionVC *atvc = (AddTransactionVC *)navController.topViewController;
atvc.delegate = self;
WMMGTransaction *addedTransaction = (WMMGTransaction *)[WMMGTransaction MR_createInContext:localContext];
addedTransaction.account = self.currentAccount.name;
atvc.thisTransaction = addedTransaction;
}
else if ([[segue identifier] isEqualToString:#"selectAccountSegue"])
{
UINavigationController *navController = (UINavigationController *)segue.destinationViewController;
AccountSelectVC *acctSelVC = (AccountSelectVC *)navController.topViewController;
acctSelVC.delegate = self;
}
}
Activation of either button segues to the appropriate view controller, but causes this warning:
Warning: Attempt to present <UINavigationController: 0x7fb99b4dd430> on <FirstViewController: 0x7fb99b565dd0> whose view is not in the window hierarchy!
I have a Save and a Cancel Navigation bar button on each View controller. Other than as mentioned above, everything works as expected, except for the Cancel button on the View controller at newTransSegue, which dismisses the VC, but crashes the app with this error:
EXC_BAD_ACCESS (code = 1, address = 0x7f87394af29)
Here is the delegate method I use to dismiss that VC:
-(void)addTransactionViewControllerDidCancel
{
[self.navigationController dismissViewControllerAnimated:YES completion:nil];
}
I've been at this for a couple of days, and have tried deleting the segues and recreating them in storyboard, as well as doing the same for the navigation controllers. I've gone off the rails somewhere, but can't see exactly where.
I could sure use some guidance. :)
OK, I studied the reference kindly provided by #Jay. Turns out I had seen it before, but only Part 1. In Part 2, I discovered a reference to Enable Zombie Objects, which I did. Now, when the app crashed, I was provided with this message: [_UILayoutGuide isDescendantOfView:], which pointed to an issue in the Storyboard.
Upon examining the Storyboard, I discovered that, surprisingly, the representation of the view controller in question was shaped differently than the surrounding view controllers. I wish I'd made a screenshot, but in the heat of the hunt, I didn't.
In any case, further research turned up this question (and its associated comments). My investigation revealed that, while I had Size Classes enabled, for some reason I can't explain, the size of the relevant View Controller under Simulated Metrics had been set to "Freeform." I reset it to "Inferred" and things appear to be operating normally now--no crashes. Wish I could explain it in detail, but I'm happy with the result!

UITabBar disappears after pushed to new view controller

I have an UITabBarController that has 3 buttons. The second button points to ViewController1 which is connected to another view called ViewController2. After I tap a button in ViewController2 I programmatically present ViewController1 again, that works perfect except one thing. After I "arrived" to ViewController1 the tab bar disappears.
I'm using this method to navigate back to ViewController1. (exactly I navigate to its navigation controller, but already tried with the view)
- (void)presentViewControllerAnimated:(BOOL)animated {
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"storyboard" bundle:nil];
UINavigationController *firstViewNavigationController = [storyboard instantiateViewControllerWithIdentifier:#"destination"];
[self presentViewController:firstViewNavigationController animated:animated completion:nil];
}
I call here the first method
- (void)didTapButton:(id)sender {
UIButton *button = (UIButton *)sender;
CGPoint pointInSuperview = [button.superview convertPoint:button.center toView:self.tableView];
[self presentViewControllerAnimated:YES];
}
This method hides the tab bar in the ViewController2, I already tried without it, therefore there is no problem with it.
-(BOOL)hidesBottomBarWhenPushed
{
return YES;
}
I can't figure out why this thing happens, I think it's a fair solution, that worked well for a several times when I needed to present views. I've read it can happen with segues, but I'm doing it with code without segues.
Actually your code works right. There should not be tab bar when you present FirstViewController from SecondViewController. Because when you call instantiateViewControllerWithIdentifier its basically creates a new instance of that view controller, and of course, there is no tab bar.
The right way to go back to your first view controller is to pop SecondViewController (or dismiss it, if it presented modally). So your final code should be like this
- (void)didTapButton:(id)sender {
// If this view controller (i.e. SecondViewController) was pushed, like in your case, then
[self.navigationController popViewControllerAnimated:YES];
// If this view controller was presented modally, then
// [self dismissViewControllerAnimated:YES completion:nil];
}
And of course, your view controller hierarchy in storyboard must be like this:
-- UINavigationController -> FirstViewController -> SecondViewController
|
->UITabBarController____|
-...
-...
I've tried the same and got the same result.
My solution was simple, on the push do this :
UINavigationController *firstViewNavigationController = [storyboard instantiateViewControllerWithIdentifier:#"destination"];
firstViewNavigationController.hidesBottomBarWhenPushed = true; // Insert this and set it to what you want to do
[self presentViewController:firstViewNavigationController animated:animated completion:nil];
and then remove your
-(BOOL)hidesBottomBarWhenPushed
{
return YES;
}

how to push a view controller to a navigation controller in a tab bar in iOS

I have a tab bar controller with a navigation controller per tab. The nav controllers have view controllers. I then have a Login View Controller not connected to the tab bar controller that I call with the code below if a user needs to login or logs out.
I am seeing some strange behavior when I try to conditionally push a view controller based on if a user is logged in or not.
My logic looks like this:
if(currentUser){
}else{
LoginViewController *svc = [self.storyboard instantiateViewControllerWithIdentifier:#"Login"];
svc.hidesBottomBarWhenPushed = YES;
[self.navigationController pushViewController:svc animated:YES];
}
When the view is pushed it look as if the the Login View is pushed and another Login View is pushed on top of it.
For logout I have the same code in a segue:
if ([segue.identifier isEqualToString:#"LogoutView"]) {
[self logOut];
LoginViewController *svc = [self.storyboard instantiateViewControllerWithIdentifier:#"Login"];
svc.hidesBottomBarWhenPushed = YES;
[self.navigationController pushViewController:svc animated:YES];
}
In this instance I see the same double push and each time I click my login button it pushes another Login View. This happens infinitely. I then get the warning:
Finishing up a navigation transition in an unexpected state.
Navigation Bar subview tree might get corrupted.
When I press back in the navigation, the app crashes with the error:
NSInvalidArgumentException', reason: 'Can't add self as subview'
Am I not correctly pushing the login view controller?
Segues instantiate new controllers, and perform the transition form the source controller to the destination controller. Therefore, you should neither be instantiating the controller in code, nor pushing it with pushViewController:animated:. If the buttons (rather than the controller) are triggering the segues, then you only need to get a reference to the destination controller (segue.destinationViewController), and use that to hide the bottom bar,
-(void)prepareForSegue:(UIStoryboardSegue *) segue sender:(id) sender {
if ([segue.identifier isEqualToString:#"LogoutView"]) {
[self logOut];
LoginViewController *svc = segue.destinationViewController;
svc.hidesBottomBarWhenPushed = YES;
}
}
If you need to do this conditionally, then the segue should be connected directly from the controller, not a button. Then you need to call performSegueWithIdentifier: in some method where you have the logic to determine which (or whether a) segue should be performed.

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;

Modal Segue Chain

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];

Resources