So I'm having problems releasing some view controllers.
In essence the dealloc for the PhotoPostViewController never seems to get called, so I can't clear down the images contained within that are munching all the memory.
This is my UIViewController subclass, I can have up to 100 of these at any one time added as subviews to the main scroll view, the iPad gets tight for memory after that.
#interface PhotoPostViewController : UIViewController {
IBOutlet UIImageView *backgroundImage;
IBOutlet UIImageView *serviceImage;
}
Then in my main view class I have a method to create these views and add them to a scrollView. This method is typically called from a loop to create all the subviews I need.
- (void) addPost {
PhotoPostViewController *postView = [[PhotoPostViewController alloc] initWithNibName:#"PhotoPostViewController" bundle:nil];
[scrollView addSubview:[postView view]];
[viewControllers addObject:postView];
}
viewControllers is an NSMutableArray created in the main class init.
scrollView is a UIScrollView on my main view.
This all works fine, I know the limit of the memory usage on the iPad and keep within that at any given time, opening Popovers to give preview images and videos etc...
Doesn't run out of memory until I try to refresh the screen.
The code to do this is:
- (IBAction)didPressRefresh:(id)sender {
for(UIView *subview in [scrollView subviews]) {
[subview removeFromSuperview];
}
for(UIViewController *c in viewControllers) {
[c release];
}
[viewControllers removeAllObjects];
}
For the sake of simplicity I clear off all the subviews and try to release them before recreating the next set of subviews using the function above.
It removes them from the view, but runs out of memory adding the new set of view controllers. In my test cases the sets of view controllers are identical in content, so if it loads from clean first time, then it should load the second time and every other time after that if I release everything properly.
What actually happens is it crashes due to low memory when creating the second set of view controllers.
While debugging I've put breakpoints on the 'viewDidUnload' and 'dealloc' methods, but they never get hit.
It looks like the UIViewController itself is getting released, yet the UIImageViews within are not, clearly they'd usually get released by my code in the dealloc (or viewDidUnload) method.
So I'm confused.
Counting things it looks to me like the reference counts are fine. so how come the dealloc is not getting hit ?
Andi
You need to send the postView object the -release message after adding it to the viewControllers collection:
- (void) addPost {
PhotoPostViewController *postView = [[PhotoPostViewController alloc] initWithNibName:#"PhotoPostViewController" bundle:nil];
[scrollView addSubview:[postView view]];
[viewControllers addObject:postView];
[postView release];
}
The reason why you need to do this is because the collection sends the -retain message to all objects that are added to it, hence the memory leak and -dealloc not being hit.
EDIT:
Your -didPressRefresh: method should look like this:
- (IBAction)didPressRefresh:(id)sender {
[[scrollView subviews] makeObjectsPerformSelector:#selector(removeFromSuperview)];
[viewControllers removeAllObjects];
}
Related
What am I doing is I am creating lots of UIView in the background and keep them in a NSMutableArray to use later. But when I dismiss the view controller I check the memory in Xcode and it seems some of memory not being released. I checked; view controller is being deallocated.
Check please:
This happend after several showing and dismissing the view controller. Some of them is being released but not all.
Thanks.
Uncheck Enable Zombie Objects option under Edit Scheme. And try again.
A zombie is an object that has been deallocated, but references to it still exist and messages are still being sent to it
I think this link has more info for you
What is NSZombie?
I suppose you use arc, so it might be useful to explicitly release this in dealloc.
-(void)dealloc {
for(UIView *vw in self.arrayOfViews) {
vw = nil;
}
self.arrayOfViews = nil;
}
Using dealloc is a bit like the old days (pre-arc), but it will help you manage memory better.
!important! --> NEVER call [super dealloc]; when using arc!
In dealloc method release all views that you have in the array.
called the below method in your controller dealloc method
- (void)releaseViewArray
{
// Releasing views in the array
for (UIView *view in _viewArray) {
[view release];
}
// Releasing the array that holding the views
[_viewArray release];
}
I've added a sub view like this in the main view:
BTLPXYPad *XYPad = [[BTLPXYPad alloc] initWithFrame: CGRectMake (30, 10, 280, 460)];
[window addSubview:XYPad];
done all my bits that i need to and then removed it using this in the BTLPXYPad class:
[self removeFromSuperview];
What I need is to perform a task once it has gone. I know that with a UIViewController type class I could use viewDidDissapear but I can't seem to find the same thing for a UIView Type. Can anyone help please?
To know when you a view has actually been removed you could implement didMoveToSuperview and check if the superview is now nil
- (void)didMoveToSuperview;
{
[super didMoveToSuperview];
if (!self.superview) {
NSLog(#"Removed from superview");
}
}
[self removeFromSuperview];
What I need is to perform a task once it has gone.
When you say [self removeFromSuperview], it is gone. There is a delay of just one runloop for it to look gone, but removing the view removes the view.
So the solution is just to proceed to your "task once it is gone":
[self removeFromSuperview];
// do your task
If it seems like the "do your task" code is in the wrong place - it isn't. The fact that you need the view to be notified when it is gone shows that your architecture was wrong to start with. A view shouldn't be performing any "task"; it is View in the Model-View-Controller structure. It just displays stuff. Your view controller is Controller; it is the one to do the task.
Nor should the Controller need to consult the view at this point, because the view should not have been storing any important data to begin with. Data is Model, and should already have been retrieve and stored by the Controller before this moment.
I have an issue that I'm stuck on, but I have no idea why it even happens; If I push a detail controller on the stack, and I swipe back very quickly using the default left edge interactivePopGestureRecognizer, my parent/root view controller's UINavigationBar looks corrupt or something, almost like the built in iOS transition mechanism didn't have time to do it's job at resetting it after the detail view is gone. Also to clarify, everything in this 'corrupt' UINavigationBar is still touchable and everything on my parent/root view controller works perfectly.
For people downvoting due to no source code: there is no source code! This is an Apple bug!
Is there anyway to reset this UINavigationBar to what it should be when the parent/root view controller's viewDidAppear method gets called?
Note that this bug does not occur if I tap the top left back button instead of using the left edge interactivePopGestureRecognizer.
Edit: I added an NSLog to check the navigationBar's subview count on viewDidAppear on the parent/root view controller, and the count is always the same, corrupt or not, so I'd like to know why the popped controller is wreaking havoc with my UINavigationBar.
If you can help me at all, I'd greatly appreciate it! Thank you.
I've attached a screenshot of what it looks like: Note that the back chevron isn't part of my parent/root view controller, it's part of what was popped off the stack. Testing123 is the title for the parent/root view controller and not that of what was popped off the stack. The head and gear icons are part of the parent/root view controller.
Edit: I've thought something like this could fix the issue, but turns out it doesn't, and is really bad experience IMO too. This is not the kind of solution I'm looking for. I'm posting a large bounty so this can be resolved correctly! 😃. I just can't have this weird UI behavior be in a production quality app.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.navigationController pushViewController:[UIViewController new] animated:NO];
[self.navigationController popToRootViewControllerAnimated:YES];
}
TL;DR
I made a category on UIViewController that hopefully fixes this issue for you. I can't actually reproduce the navigation bar corruption on a device, but I can do it on the simulator pretty frequently, and this category solves the problem for me. Hopefully it also solves it for you on the device.
The Problem, and the Solution
I actually don't know exactly what causes this, but the navigation bar's subviews' layers' animations seem to either be executing twice or not fully completing or... something. Anyway, I found that you can simply add some animations to these subviews in order to force them back to where they should be (with the right opacity, color, etc). The trick is to use your view controller's transitionCoordinator object and hook into a couple of events – namely the event that happens when you lift your finger up and the interactive pop gesture recognizer finishes and the rest of the animation starts, and then the event that occurs when the non-interactive half of the animation finishes.
You can hook into these events using a couple methods on the transitionCoordinator, specifically notifyWhenInteractionEndsUsingBlock: and animateAlongsideTransition:completion:. In the former, we create copies of all of the current animations of the navbar's subviews' layers, modify them slightly, and save them so we can apply them later when the non-interactive portion of the animation finishes, which is in the completion block of the latter of those two methods.
Summary
Listen for when the interactive portion of the transition ends
Gather up the animations for all the views' layers in the navigation bar
Copy and modify these animations slightly (set fromValue to the same thing as the toValue, set duration to zero, and a few other things)
Listen for when the non-interactive portion of the transition ends
Apply the copied/modified animations back to the views' layers
Code
And here's the code for the UIViewController category:
#interface UIViewController (FixNavigationBarCorruption)
- (void)fixNavigationBarCorruption;
#end
#implementation UIViewController (FixNavigationBarCorruption)
/**
* Fixes a problem where the navigation bar sometimes becomes corrupt
* when transitioning using an interactive transition.
*
* Call this method in your view controller's viewWillAppear: method
*/
- (void)fixNavigationBarCorruption
{
// Get our transition coordinator
id<UIViewControllerTransitionCoordinator> coordinator = self.transitionCoordinator;
// If we have a transition coordinator and it was initially interactive when it started,
// we can attempt to fix the issue with the nav bar corruption.
if ([coordinator initiallyInteractive]) {
// Use a map table so we can map from each view to its animations
NSMapTable *mapTable = [[NSMapTable alloc] initWithKeyOptions:NSMapTableStrongMemory
valueOptions:NSMapTableStrongMemory
capacity:0];
// This gets run when your finger lifts up while dragging with the interactivePopGestureRecognizer
[coordinator notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context) {
// Loop through our nav controller's nav bar's subviews
for (UIView *view in self.navigationController.navigationBar.subviews) {
NSArray *animationKeys = view.layer.animationKeys;
NSMutableArray *anims = [NSMutableArray array];
// Gather this view's animations
for (NSString *animationKey in animationKeys) {
CABasicAnimation *anim = (id)[view.layer animationForKey:animationKey];
// In case any other kind of animation somehow gets added to this view, don't bother with it
if ([anim isKindOfClass:[CABasicAnimation class]]) {
// Make a pseudo-hard copy of each animation.
// We have to make a copy because we cannot modify an existing animation.
CABasicAnimation *animCopy = [CABasicAnimation animationWithKeyPath:anim.keyPath];
// CABasicAnimation properties
// Make sure fromValue and toValue are the same, and that they are equal to the layer's final resting value
animCopy.fromValue = [view.layer valueForKeyPath:anim.keyPath];
animCopy.toValue = [view.layer valueForKeyPath:anim.keyPath];
animCopy.byValue = anim.byValue;
// CAPropertyAnimation properties
animCopy.additive = anim.additive;
animCopy.cumulative = anim.cumulative;
animCopy.valueFunction = anim.valueFunction;
// CAAnimation properties
animCopy.timingFunction = anim.timingFunction;
animCopy.delegate = anim.delegate;
animCopy.removedOnCompletion = anim.removedOnCompletion;
// CAMediaTiming properties
animCopy.speed = anim.speed;
animCopy.repeatCount = anim.repeatCount;
animCopy.repeatDuration = anim.repeatDuration;
animCopy.autoreverses = anim.autoreverses;
animCopy.fillMode = anim.fillMode;
// We want our new animations to be instantaneous, so set the duration to zero.
// Also set both the begin time and time offset to 0.
animCopy.duration = 0;
animCopy.beginTime = 0;
animCopy.timeOffset = 0;
[anims addObject:animCopy];
}
}
// Associate the gathered animations with each respective view
[mapTable setObject:anims forKey:view];
}
}];
// The completion block here gets run after the view controller transition animation completes (or fails)
[coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
// Iterate over the mapTable's keys (views)
for (UIView *view in mapTable.keyEnumerator) {
// Get the modified animations for this view that we made when the interactive portion of the transition finished
NSArray *anims = [mapTable objectForKey:view];
// ... and add them back to the view's layer
for (CABasicAnimation *anim in anims) {
[view.layer addAnimation:anim forKey:anim.keyPath];
}
}
}];
}
}
#end
And then just call this method in your view controller's viewWillAppear: method (in your test project's case, it would be the ViewController class):
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self fixNavigationBarCorruption];
}
After investigating this issue for some time with debug console, Instruments and Reveal, I have found out the following:
1) On simulator the bug can be recreated every time, if using Profile/Automation Template and adding the following script:
var target = UIATarget.localTarget();
var appWindow = target.frontMostApp().mainWindow();
appWindow.buttons()[0].tap();
target.delay(1);
target.flickFromTo({x:2, y: 100}, {x:160, y: 100});
2) On real device (iPhone 5s, iOS 7.1) this script never causes the bug. I tried various options for flick coordinates and the delay.
3) UINavigationBar consists of:
_UINavigationBarBackground (doesn't seem to be related to the bug)
_UIBackdropView
_UIBackgropEffectView
UIView
UIImageView
UINavigationItemView
UILabel (visible in the bug)
_UINavigationBarBackIndicatorView (visible in the bug)
4) When bug happens UILabel looks half transparent and in the wrong position, but the actual properties of the UILabel are correct (alpha: 1 and frame as in normal situation). Also _UINavigationBarBackIndicatorView looks doesn't correspond to actual properties - it is visible although it's alpha is 0.
From this I conclude that it's a bug of Simulator and that you can't even detect from the code that something is wrong.
So #troop231 - are you 100% sure this also happens on device?
Key Concept
Disable gesture recognizer when pushing view controller, and enable it when view appeared.
A Common Solution: Subclassing
You can subclass UINavigationController and UIViewController to prevent corruption.
MyNavigationController : UINavigationController
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
[super pushViewController:viewController animated:animated];
self.interactivePopGestureRecognizer.enabled = NO; // disable
}
MyViewController : UIViewController
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.navigationController.interactivePopGestureRecognizer.enabled = YES; // enable
}
Problem: Too annoying
Need to use MyNavigationController and MyViewController instead of UINavigationController and UIViewController.
Need to subclass for UITableViewController, UICollectionViewController, and more.
A Better Solution: Method Swizzling
It could be done by swizzling UINavigationController and UIViewController methods. Want to know about method swizzling, visit here.
Example below uses JRSwizzle that makes method swizzling easy.
UINavigationController
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self jr_swizzleMethod:#selector(viewDidLoad)
withMethod:#selector(hack_viewDidLoad)
error:nil];
[self jr_swizzleMethod:#selector(pushViewController:animated:)
withMethod:#selector(hack_pushViewController:animated:)
error:nil];
});
}
- (void)hack_viewDidLoad
{
[self hack_viewDidLoad];
self.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>)self;
}
- (void)hack_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
[self hack_pushViewController:viewController animated:animated];
self.interactivePopGestureRecognizer.enabled = NO;
}
UIViewController
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self jr_swizzleMethod:#selector(viewDidAppear:)
withMethod:#selector(hack_viewDidAppear:)
error:nil];
});
}
- (void)hack_viewDidAppear:(BOOL)animated
{
[self hack_viewDidAppear:animated];
self.navigationController.interactivePopGestureRecognizer.enabled = YES;
}
Being Simple: Use Open Source
SwipeBack
SwipeBack does it automatically without any code.
With CocoaPods, just add a line below into your Podfile. You don't need to write any code. CocoaPods automatically import SwipeBack globally.
pod 'SwipeBack'
Install pod, and it's done!
My question seems to need a real experienced iOS developer's advice.
In my app I've implemented a Container View Controller (class: CC_CosyLogContainerVC), which consists of 3 subviews:
a header subview with Buttons to activate 1 of 3 child view controllers,
a middle subview, which displays some data and
a bottom subview, which is the placeholder for the child view controllers.
The container view controller is embedded in a UINavigationController, which also gets accessed by it's child view controllers, when they become active. The child view controllers set the navigationController's navigationItem.title and navigationItem.rightBarButtonItems according to their needs.
This whole management of activating and deactivating the child view controllers runs very well. There are no memory leaks.
But I've detected a performance issue, which I further examined with Instruments. That is, why I am able now to understand the problem and to describe it here. Unfortunately I have no idea how to solve it!
At the beginning, when the container view controller did load, the child view controllers also display their views very fast. When I tap a button in the container's header subview to switch to another child view controller, this happens really fast - but only at the beginning. I discovered, that each time I do such a child switch, this takes more and more time.
Using Instruments I've found, that the "Button dance" in the NavigationBar is responsible for this time consumption. Each time, when one of the child view controller's main view appears, they call a method to configure the available Buttons for the NavigationBar.
As a typical example, I show the code for "addCameraButton":
- (void)addCameraButton
{
if ((self.cosyLogID) && ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])) {
if (!self.cameraButtonAdded) {
//add new Camera Button
UINavigationItem *myNavigationItem = (self.parentViewController ? self.parentViewController.navigationItem : self.navigationItem);
NSMutableArray *newArray = [[NSMutableArray alloc] initWithArray:myNavigationItem.rightBarButtonItems];
[newArray addObject:self.cameraButton];
myNavigationItem.rightBarButtonItems = newArray;
self.cameraButtonAdded = YES;
}
} else {
//remove old Camera Button if it already exists
[self removeCameraButton];
}
}
The performance critical line of code is
myNavigationItem.rightBarButtonItems = newArray;
Instruments time profiler shows, that this line consumes 99,9% of the whole method. On an iPhone4, [addCameraButton] needs about 50 ms of time, when it is called the first time. The second call (when I switched to another child and then switch back to this one) takes about 75 ms of time.
By extensive testing and manual switching between the childs, the needed time to set the rightBarButtonItems array grows and grows up to 500 ms and more - there seems to be no limit.
This leads to a bad responsiveness, as the app gets slower and slower. The appearing of the childs' views takes more and more time because of this navigation buttons issue.
How can advise and has an idea, how I can resolve this growing time consumption issue when setting the rightBarButtonItems?
Additional information:
Method [removeCameraButton], which is the counterpart for [addCameraButton] and called in child's [viewWillDisappear]:
- (void)removeCameraButton
{
UINavigationItem *myNavigationItem = (self.parentViewController ? self.parentViewController.navigationItem : self.navigationItem);
NSArray *oldArray = myNavigationItem.rightBarButtonItems;
NSMutableArray *newArray = [[NSMutableArray alloc] init];
BOOL doAdd;
for (id myObject in oldArray) {
doAdd = YES;
if ([myObject isKindOfClass:[UIBarButtonItem class]]) {
if ([(UIView *)myObject tag] == CC_TAG_FOR_LOGFOTOSVIEW_BTN_CAMERA) {
doAdd = NO;
}
}
if (doAdd) {
[newArray addObject:myObject];
}
}
myNavigationItem.rightBarButtonItems = newArray;
self.cameraButtonAdded = NO;
}
Observation of the rightBarButtonItems array:
I did check [newArray count] in the debugger. It does not grow over the cycles, it definitely always contains the right number and kind of UIBarButtonItems.
The Getter method for the camera button:
-(UIBarButtonItem *)cameraButton {
if (!_cameraButton) {
_cameraButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCamera target:self action:#selector(cameraButtonPressed:)];
_cameraButton.tag = CC_TAG_FOR_LOGFOTOSVIEW_BTN_CAMERA;
}
return _cameraButton;
}
I have a simple question regarding ARC. I show a UIView if a user taps a button using addSuperView within a UIViewController. The UIView contains a close button, if tapped I want to remove the view.
I used to call a method within the UIViewController after animating the view offscreen:
- (void)viewDidClose:(UIView *)view
{
[view removeFromSuperview];
[view release], view = nil;
}
Now using ARC I changed it to:
- (void)viewDidClose:(UIView *)view
{
[view removeFromSuperview];
view = nil;
}
The question now is: I want to remove the protocol and the delegation to the view controller and do this within the UIView itself.
Pre-ARC (within view):
- (void)didStop
{
[self removeFromSuperview];
[self autorelease];
}
I can't use 'autorelease' in ARC nor set 'self = nil', as far as I know ARC comes in place as soon as I set the view to nil or replace it, but what if I don't replace it? Is [view removeFromSuperview] enough to take care of everything or does this leak?
Thanks a lot! I appreciate any help!
(Note that in your non-ARC version, you'd want the autorelease before the removeFromSuperview).
I've wondered about similar things. The 'danger' is that when you do the removeFromSuperview without first doing an autorelease, the object is immediately deallocated, even before the end of the method. That's skeezy.
My idea was that you'd make the method return self. In the non-ARC case, you'd create this pointer with autorelease but ARC should do that for you. You may not need the pointer in the caller, but I believe this will force ARC to do what you want.