The root level view controller in my iPad application is a UISplitViewController. Thus, it has 2 view controllers :
one master view controller (item 0 of the viewControllers property)
one detail view controller (item 1 of the viewControllers property)
The detail view controller is a custom view controller that I change depending on taps and events in my master view controller.
To change this detail view controller, I use the following code:
- (void)replaceSecondViewControllerBy:(UIViewController *)viewController {
[[self.viewControllers objectAtIndex:1] dismissModalViewControllerAnimated:NO];
NSArray *newVC = [NSArray arrayWithObjects:[self.viewControllers objectAtIndex:0], viewController, nil];
self.viewControllers = newVC;
}
My problem is that when my app receives a memory warning event, the didReceiveMemoryWarning method is called for all my view controllers, except for former detail view controllers. And they're not being deallocated because they still are delegates for other objects (including asynchronous methods that might still be running).
My questions are :
What are the rules for a UIViewController to receive a didReceiveMemoryWarning message ? Why don't my former detail view controllers receive them ?
Can I safely call didReceiveMemoryWarning or viewDidUnload myself on these old view controllers ?
It looks like a UIViewController subscribes to UIApplicationDidReceiveMemoryWarningNotification when it is created. It removes observing the notification when it is deallocated. So didReceiveMemoryWarning is called even if the the controller's view is not in the view hierarchy. So make sure your controllers are not deallocated.
It is unlikely but if you happen to be using the following code to remove notification observation from your controllers they will also stop listening memory notifications.
[[NSNotificationCenter defaultCenter] removeObserver:controller name:nil object:nil];
Related
I have a UITabBarController with four tabs. In each of the view controllers presented when a tab is selected I have a reset button. Tapping the button will change the appearance of all the view controllers. In particular, it will change the text of some labels in the different view controllers.
Is there some recommended way to update all the view controllers of a UITabBarController at the same time i.e. to make them reload their views?
My current approach is to make those view controllers conform to a protocol
#protocol XYReloadableViewController
- (void)reloadContents;
#end
and then send the message -reloadContents to all the view controllers when the button is tapped:
- (IBAction)touchUpInsideResetButton {
// ...
NSArray *viewControllers = self.tabBarController.viewControllers;
for (UIViewController<XYReloadableViewController> *viewController in viewControllers) {
[viewController reloadContents];
}
}
Then in each of the view controllers I would have to implement that method:
- (void)reloadContents {
[self.tableView reloadData];
// other updates to UI ...
}
But that seems a little too complicated. So is there an easier way to tell the view controllers to reload their views?
Edit: And what happens if I present a UINavigationController in some of the tabs or a container view controller? I would need to pass the message along the chain of all its child view controllers...
You can create ReloadViewController and all you contrlollers inheritance
from him.
ReloadViewController have property UIButton and methods:
-(void)reloadContents;
-(IBAction)touchUpInsideResetButton:(id)sender;
in .m file:
-(void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(reloadContents)
name:#"MyNotification"
object:nil];
}
- (IBAction)touchUpInsideResetButton:(id)sender
{
[[NSNotificationCenter defaultCenter] postNotificationName:#"MyNotification"
object:nil];
}
in your viewControllers need only override method reloadContents
Notifications sound like a better fit for this. When view controllers need to be reset, broadcast an NSNotification and have any view controllers that might need to reset themselves listen for that notification, and trigger what they need to do. That way it doesn't matter how far down a navigation stack they are.
You might want to defer updates until the view actually appears. You could set a BOOL needsUpdate when the VCs receive the notification, but only do the actual update in viewWillAppear:, to save resources and prevent a large number of updates from going off at once (and perhaps blocking the main thread).
If this behaviour is common to all your view controllers, make a UIViewController subclass to prevent repeating code and have them all inherit from that. Alternatively, (if you're using Apple VC subclasses) make a category on UIViewController to add the notification methods.
I'm currently working on a project which implements a custom navigation controller, whose code is here:
https://gist.github.com/emilevictor/724a6602fedb8100650c
In one of my controllers, which gets pushed to the navigationController via a push segue, I have an action on a button to return to the main screen:
- (IBAction)returnToMainScreen:(id)sender
{
NSArray *returnedControllers = [self.navigationController popToRootViewControllerAnimated:YES];
NSLog(#"Popped to root view controller.");
}
This will return the current view controller and one preceding it in the returnedControllers array.
However, it doesn't change screens, or call any viewDidDisappear functions. Anyone know why?
Make sure you child viewControllers are being added to the parent viewController with the method [UIViewController addChildViewController:]
viewController's view is not loaded just after that viewController is pushed into navigation controller.
This is my code snippet.
- (void)myMethodInClassA {
// window's root view controller is navigation controller
UINavigationController *naviCtrl = (UINavigationController*)[UIApplication sharedApplication].keyWindow.rootViewController;
MyViewController *myVC = [[MyViewController alloc] initWithNibName:#"MyViewController" bundle:nil];
[naviCtrl pushViewController:myVC animated:NO];
// at this point, myVC's view is NOT loaded
}
When I call myMethodInClassA, myVC's viewDidLoad is called AFTER that method returns. I'd expected that myVC's view is loaded just after navigation controller's pushViewController:animated: is called and before myMethodInClassA returns.
When exactly view controller's view is loaded? Apple's documentation just says it is loaded when it is first accessed. It's a bit ambiguous. why doesn't navigation controller's pushViewController: access view controller's view?
p.s. sorry for initial ambiguous question.
Pushing a view controller (VC) onto a navigation controller's stack makes the VC into a child view controller of the navigation controller (which is a container view controller). Creating such a child-parent relationship is a distinct step which does not cause the child VC's view to be loaded immediately. Rather the container VC loads the view at a later time. I believe there is no explicit specification for what "later" means - usually it will be when the container VC has decided that the time has come to integrate the child VC's view into the container VC's view hierarchy. But basically it simply happens at the discretion of the container VC's implementation.
That being said, anyone can force a VC's view to be loaded by simply accessing the VC's view property. For instance, in your code you could add this line
myVC.view;
which would trigger loadView and then viewDidLoad in MyViewController.
However, in your case if MyViewController needs to react to the event that it has been associated with a container VC, then it would be better to override one (or both?) of the following methods in MyViewController:
- (void) willMoveToParentViewController:(UIViewController*)parent
{
// write your code here
}
- (void) didMoveToParentViewController:(UIViewController*)parent
{
// write your code here
}
You need to be aware, though, that willMoveToParentViewController and didMoveToParentViewController are also invoked when MyViewController is popped from its parent navigation controller's stack. You can detect that this is the case by checking the parent argument for nil.
(Swift 2)
Since this question doesn't have an accepted answer...
What I ended up doing is create a convenience init at the child view controller:
convenience init() {
self.init(nibName: "ChildViewController", bundle: nil)
//initializing the view Controller form specified NIB file
}
and in the parentViewController's viewDidLoad():
let commentsView = CommentsViewController()
self.addChildViewController(commentsView)
self.momentsScrollView.addSubview(commentsView.view)
commentsView.didMoveToParentViewController(self)
As stated above,viewDidLoad gets called once when a view is pushed,you might want to do your stuff in viewWillAppear or viewDidAppear.
Ya if that ViewController will be already pushed in navigationController stack then ViewDidLoad method will not be called again.
First time when you will push that ViewController then viewDidLoad will be called.
So if you need that your some functionality is to be executed every time then implement it in viewWillAppear method because it will be called every-time you push your viewController.
Hope it helps you.
are you pushing the view controller for the first tym?if YES then only viewDidLoad() of the controller will be called and if its already pushed and this is not the first tyn then viewWillAppear () will be called.(or) if you are making a new instance every tym u push it then viewDidLoad() will be called.
I find that I have to call loadViewIfNeeded()
https://developer.apple.com/documentation/uikit/uiviewcontroller/1621446-loadviewifneeded
I am working with Automatic Reference Counting.
I have a custom UIViewController subclass and whenever I call -presentViewController: animated:completion: or remove its view from the superview I would like to NSLog something like "I am dealloced" so I know that the view controller has successfully been removed. I have implemented the -dealloc method in my view controller. However I started a test project where I just had two UIViewController instances (no retain cycles) and -dealloc is not called either when I push the second UIViewController modally or when I remove the superview or when I remove it from the parent view controller. Am I missing something ? In my original project (not the test case) Instruments shows me that those controllers leave a memory footprint that I can't get rid off.
If you want to switch view controllers, and have the one you're switching away from be deallocated, then just switch the root view controller of the window. So, if you're in VC1 and want to go to VC2, then do this in VC1:
VC2 *vc2 = [[VC2 alloc] init]; // or however else is appropriate to get an instance of this class
self.view.window.rootViewController = vc2;
If you haven't created any property to point to vc1, then it will be deallocated after making this switch.
If you want to use a modal presentation or a modal segue (to get the animation when you switch controllers), you can still get the initial controller to be deallocated by switching the root view controller after the presentation from the viewDidAppear method of vc2:
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.view.window.rootViewController = self;
}
To get a print when the View Controller is deallocated you can implement the dealloc method as
- (void) dealloc {
NSLog(#"The instance of MyViewController was deallocated");
}
Then to get a print when the View Controller left the view you can implement
- (void) viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
NSLog(#"The instance of MyViewController left the main view")
}
If you use -presentViewController:animated:completion: you are retaining the parentViewController every time you call this method. ModalViewControllers are simply pushed on top of the other ViewController.
ModalViewControllers are only designed for some kind of information / User Input and stuff like that. If you want to dealloc the ParentViewController you have to deal with your own implementation.
dealloc method isn't called when the class is retained (or something in this class is retained) and not reeleased. It is justly for projects with both ARC and without it. So check your code twice.
I have a view controller that contains a table view, the items in the table can be selected and a detail view controller duly created.
The items in the table represent items that can have a time based trigger associated with them and a local notification is scheduled for each item, if the app is in the foreground when a local notification expires then the detail view for the item is automatically displayed.
I have a problem that manifests when two notifications expire at the same time which results in the views not being displayed properly and in addition the console logs:
"Unbalanced calls to begin/end appearance transitions for NNN" where NNN is my detail view controller.
The table view controller is created as follows:
self.tableViewController = [[TableViewController alloc] initWithNibName:#"TableView" bundle:nil];
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:self.tableViewController];
self.window.rootViewController = navController;
When a local notification expires and didReceiveLocalNotification: is invoked the app broadcasts a notification using NSNotifcationCenter postNotificationName: and to which the table view controller is listening for. When the table view controller receives that notification it creates the detail view controller and pushes it to the stack as:
[self.navigationController pushViewController:detailViewController animated:YES];
I read somewhere that there could be a problem if a view controller pushes another view controller when it itself is not on the top of the stack - so I thought this must be the problem, because when the table view controller receives the 2nd notification it will no longer be on the top of the navigation stack because it will have previously just pushed a detail view controller onto the stack when the first notification arrived.
So I changed the push code to this:
[[self.navigationController topViewController].navigationController pushViewController:detailController animated:YES];
But it made no difference.
So I next thought there could be a problem because the first detail view controller was not getting the chance to fully display before the 2nd
view controller was pushed - so I changed my app's notification posting from using:
[[NSNotificationCenter defaultCenter] postNotificationName:
to
[[NSNotificationQueue defaultQueue] enqueueNotification: postingStyle:NSPostWhenIdle]
So that the pushes wouldn't occur within the same iteraction of the app loop. But that made no difference, nor did attempting to introduce a delay to the pushing of the detail view controlle:
double delayInSeconds = 0.1;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[[self.navigationController topViewController].navigationController pushViewController:detailController animated:YES];
});
I've no idea what the problem is or what to try next, any ideas?
"The unbalanced calls to begin/end appearance transitions"
occurs when you try and display a new viewcontroller before the current view controller is finished displaying. You can reproduce it by navigating in viewWillAppear.
Basically you are trying to push two view controllers onto the stack at almost the same time. Suggest you maintain a queue in the tableview controller which maintains a list of the detail views which need displaying. Push one at a time on to the stack and check on exit from the current detail view whether there are any queued detail views which need displaying.
This kind of navigation is going to be confusing for the user. It may be better to consider making your detail view support multiple items.
'Unbalanced calls to begin/end appearance transitions for '
Says an animation is started before the last related animation isnt done. So, are you popping any view controller before pushing the new one ? Or may be popping to root ? if yes try doing so without animation. i.e,
[self.navigationController popToRootViewControllerAnimated:NO];
And see if this resolves the issue, In my case this did the trick.
"The unbalanced calls to begin/end appearance transitions" error occurs when you try and display a new viewcontroller before the current view controller is finished displaying.
So, you have to be sure you wont present a new VC until the first finishes its animations.
Use didShowViewController and willShowViewController to block presenting a new VC before the old one completes its animation. It's for backButtonAction who makes a popViewController with Animation: YES.
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
[self.myNavView.backButton addTarget:self action:#selector(backButtonAction) forControlEvents:UIControlEventTouchUpInside];
}
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
[self.myNavView.backButton removeTarget:self action:#selector(backButtonAction) forControlEvents:UIControlEventTouchUpInside];
}
In my case, I was implementing a custom container view controller, and I was getting the warning when swapping child view controllers, despite the fact that I was calling both beginAppearanceTransition(_:animated:) and endAppearanceTransition() on both the entering and the exiting view controllers.
I solved it by reordering the method calls for the entering view controller; from:
addChildViewController(newContent)
targetContainer.insertSubview(newContent.view, at: 0)
newContent.beginAppearanceTransition(true, animated: animated)
// Called AFTER adding subview
to:
// Called BEFORE adding subview
newContent.beginAppearanceTransition(true, animated: animated)
addChildViewController(newContent)
targetContainer.insertSubview(newContent.view, at: 0)
It can also happen when you try to pop a VC from the stack more than once. On my case, the method that popped the VC was mistakenly being called multiple times. Once I cleaned that up the issue went away.
Actually you need to wait till the push animation ends. So you can delegate UINavigationController and prevent pushing till the animation ends.
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated{
waitNavigation = NO;
}
-(void)showGScreen:(id)gvc{
if (!waitNavigation) {
waitNavigation = YES;
[_nav popToRootViewControllerAnimated:NO];
[_nav pushViewController:gvc animated:YES];
}
}
As mentioned in above answers this error accuses when we try to push a view controller before the previous view controller is finish loading, so one solution is that try to push controller after it finish loading, i.e. move your push view controller code from viewWillAppear to viewDidAppear
You can add a breakpoint to
-[UIViewController _endAppearanceTransition:]
from where UIKit prints
"Unbalanced calls to begin/end appearance transitions for %#."
Look at these overloads:
If they are empty, then remark them and rebuild.
- (void) beginAppearanceTransition:(BOOL) isAppearing animated:(BOOL)animated {}
- (void) becomeActive:(NSNotification *) notification {}
In my case the problem is that I am presenting a viewController (alert) on an already presented viewController (full screen sheet).
UIKit walks up the viewController parent chain but not the presenting chain so there is a mismatch when reaching the presented and the window root view controller.
Manually calling beginAppearanceTransition in these cases made the message go away, though is seems rather a patch for a symptom than a real remedy for the error.
In React native I am getting this error while opening galley or camera screen. I set timeout before opening this screen and its works for me.
My UINavigationController was being presented, so had to replace UIModalPresentationCurrentContext with UIModalPresentationFullScreen on the presentation mechanism. Or you can use UIModalPresentationAutomatic if you have iOS 13.0 or more recent.
Are you using the appearance proxy feature?
I have found very problematic this feature, last time I have a
"Unbalanced calls to begin/end appearance transitions for"
I solved it removing the [[UITextField appearance] ..] methods.