I am adding an animated UIImageView (an explosion) to a view but it seems to not be retained. Here is the implementation of the Explosion class, a subclass of UIImageView:
#implementation Explosion
#define EXPLOSION_DIMENSION 20
- (instancetype)initAtPoint:(CGPoint)point {
if (self = [super initWithFrame:CGRectMake(0, 0, EXPLOSION_DIMENSION, EXPLOSION_DIMENSION)]) {
self.center = point;
self.image = [UIImage imageNamed:#"explosion.png"];
}
return self;
}
- (void)boom {
self.alpha = 0;
[UIView animateWithDuration:0.5
delay:1.0
options: UIViewAnimationOptionCurveLinear
animations:^{
self.alpha = 1;
}completion:^(BOOL finished){
NSLog(#"here");
// [self removeFromSuperview];
}];
}
#end
When hitting a breakpoint on NSLog(#"here"), the debugger shows no self. Huh? Any reference to self in the completion code causes a crash so I can't do a [self removeFromSuperview] there.
Here is the code in the view where it is instantiated:
Explosion * explosion = [[Explosion alloc] initAtPoint:p];
[_explosionImageViews addObject:explosion];
[self addSubview:explosion];
[explosion boom];
NSLog(#"subviews: %d array: %d", self.subviews.count, _explosionImageViews.count);
The view contains 8 other subviews. The output looks like this:
subviews: 9 array: 1
subviews: 10 array: 2
subviews: 9 array: 3
Note that the number of subviews resets to 9. Nowhere do I remove the explosion from its superview, it just seems to magically go away (garbage collection?). Sometimes the subviews.count gets up to 23 before it magically resets to 9.
I put in the _explosionImageViews array just to retain the objects so they would not go out of scope even though it seems that being added to the subviews of the view should already be doing this.
The animation is supposed to stay around for 0.5 seconds but stays much longer. Then several of them all disappear at the same time. Again, garbage collection?
Any ideas what is going on?
You're right--your explosion object is not being retained.
To you retain it, use a strong property:
#property (strong, nonatomic) Explosion *explosion;
Then, instantiate it in your implementation, like so:
self.explosion = [[Explosion alloc] initAtPoint:p];
Of course, if your explosionImageViews are being retained, then follow that property, to see if it is being released somewhere.
I hope that helps!
Related
I have a UIViewController with many subviews like UILabels, UIImages and a UIWebview. With a defined action by the user, the subviews of the UIViewController animate to different sizes and different locations inside of the UIViewController's view. Is it possible that this can be undone with a different defined action by the user? I want to make all the subviews revert back to their previous locations and sizes that they were before the animation was run. I thought of two possible solutions:
Get the properties of the subviews with the view.subviews() method before the animation is run, and then set the subviews after the animation to the properties in this array, or,
Call a method on the UIViewController to tell it to redraw all the subviews according to the properties set in the storyboard file.
Are these the right way of accomplishing what I would like to do? And if so, how would I go about doing this? (I don't know how to programmatically implement either of my ideas.)
Any help is greatly appreciated. Thanks.
Here is the solution.
#interface ViewController ()
#property (strong, nonatomic) NSMutableArray *frames;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//Saving initial frames of all subviews
self.frames = [NSMutableArray new];
NSArray *allViews = [self allViewsOfView:self.view];
for (UIView *view in allViews) {
CGRect frame = view.frame;
NSValue *frameValue = [NSValue valueWithCGRect:frame];
[self.frames addObject:frameValue];
}
}
- (NSMutableArray *)allViewsOfView:(UIView *)view
{
NSMutableArray *result = [NSMutableArray new];
[result addObject:view];
for (UIView *subView in view.subviews) {
[result addObjectsFromArray:[self allViewsOfView:subView]];
}
return result;
}
- (void)resetFrames
{
NSArray *allViews = [self allViewsOfView:self.view];
for (UIView *view in allViews) {
NSValue *frameValue = [self.frames objectAtIndex:[allViews indexOfObject:view]];
CGRect frame = [frameValue CGRectValue];
view.frame = frame;
}
}
#end
Call [self resetFrame]; whenever you want to revert view's frames back to their initial values.
You could cache all your subview's frame before changing it and running the animation, in this way you can even cache more than one action. A stack structure will be perfect for this, but there is no way to achieve this in interface builder, you have to reference outlets from IB to code to get their frame.
I added about 500 views to my viewController.view.
This action took about 5 seconds on target.
Now I want the screen to refresh after each subview I'm adding, so the user will see them appears one by one on screen.
I tried this in my viewController:
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
for(int i=0; i<500; i++)
{
//...Create aView
[self.view addsubview:aView];
[self.view setNeedsDisplay];
}
}
I run it and nothing happened for 5 seconds then all views appeared at once.
I made sure that [self.view setNeedsDisplay] called from the main thread context.
Any idea how to make those subviews appear one by one?
I found a simple solution. I added a new property to my viewController - 'subviewsCount' witch was initialised to 500. then called the following method from viewDidLoad:
-(void) addSubviewsToMotherView
{
self.subviewsCount -=1;
if (self.subviewsCount >= 0)
{
[UIView animateWithDuration:0.0
delay:0.0
options:UIViewAnimationOptionLayoutSubviews
animations:^{
[self methodToAddSubview];
}
completion:^(BOOL finished){
[self addSubviewsToMotherView];
}
];
}
}
Hi everyone,
small question.
I have an UIView class that I created, AnimatedUIView.
In the init, I create a subview that I animate like this:
- (void)animateView{
[UIView animateWithDuration:4.0
delay:1.0
options: nil
animations:^{
// Animation PART 1
}
completion:^(BOOL completed){
[UIView animateWithDuration:4.0
animations:^{
// Animation PART 2
}
completion:^(BOOL completed){
[self animateView];
}
];
}
];
}
I have to call [self animateView] myself in the second completion block rather than using option repeat because I want to delay 1 second only the first animation.
Everything works fine, I alloc and init my view and add it as a subview, and it is animated as should.
But, when I close my superview, I guess ARC does its work and deallocs the animatedView, but my CPU goes 100 % !
I have investigated and if I comment the call to [self animateView], my CPU doesn't skyrocket when I close the superview.
So I've managed to solve the problem (by putting a condition before the call and changing a boolean value before closing the superview, keepAnimating = NO) , I just wanted to understand WHY it does this?
Why does it try to keep animating, and why does it use so much CPU?
If I put an NSLog in the completion block, I first see it every 8 seconds, but when I close the superview, the NSLog just keeps appearing every ms...
BTW: it relates to this question : UIView animation using 90%CPU , which was not really answered. Thanks a lot!
CPU going to 100% almost always means infinite recursion.
We can only guess because only Apple knows what's inside the animation source code.
In this case I guess this could be caused by triggering the animation when the animation cannot run anymore (e.g. there is no layer since we are in dealloc state). That would mean the completion handler is called immediately and that leads to infinite recursion.
There are safer ways to create infinite animations (e.g. using CAAnimation on the layer).
You should not use self in side the block. It will create a retain cycle. If it is necessary to call self create a weak self pointer. Code like this
__weak typeof(self) weakself = self;
[UIView animateWithDuration:4.0
delay:1.0
options: nil
animations:^{
// Animation PART 1
}
completion:^(BOOL completed){
[UIView animateWithDuration:4.0
animations:^{
// Animation PART 2
}
completion:^(BOOL completed){
[weakself animateView];
}
];
}
];
But main issue with your code is you calling animateView recursively without any base case. So i consuming CPU cycles... do not use animateView without base case.
Try using timer
Write following line in your viewDidLoad or any where else from where you want animation to start.
[NSTimer scheduledTimerWithTimeInterval:9 target:self selector:#selector(animateView) userInfo:nil repeats:YES];
// 9 is total animation duration.....
update your animateView method as follows:
- (void)animateView{
[UIView animateWithDuration:4.0
delay:1.0
options: nil
animations:^{
// Animation PART 1
}
completion:^(BOOL completed){
[UIView animateWithDuration:4.0
animations:^{
// Animation PART 2
}
completion:^(BOOL completed){
}
];
}
];
}
I have a few custom UIView objects that all handle drawing like this:
- (void)drawRect:(CGRect)rect {
// ^ I init the layers 1 and 2
[self.layer insertSublayer:layer1 atIndex:0]; // 1 or more
[self.layer insertSublayer:layer2 atIndex:1];
}
They also have a - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; with nothing else inside but a NSLog.
I add them all inside my main ViewController like so:
- (void)viewDidLoad
{
[super viewDidLoad];
CustomView *myViewWith1Layer = [[CustomView alloc] initWithFrame:CGRectMake(20, 20, 440, 260)];
[self.view addSubview:myViewWith1Layer];
CustomViewLayered *myViewWith2Layer = [[CustomViewLayered alloc] initWithFrame:CGRectMake(40, 260, 200, -120)];
[self.view addSubview:myViewWith2Layers];
}
When I run my app, if I tap on a view that has only a single layer - I get my NSLog to show, and everything's fine. If, on the other hand, I tap on views with 1+ layers, the app crashes (objc_msgSend log shows up with "EXC_BAD_ACCES (code=1, address=..."). I guess this is somehow related with ARC, which I have enabled.
How do I add multiple layers to a view, without it being messed up by ARC?
I don't think that this is an ARC problem, but creating and inserting the layers in drawRect
is wrong. This should be done (only once) in the init method of the view, e.g. in initWithFrame.
In my case, the solution was definitely related to ARC.
When initialising my delegates, I immediately assigned them to the layer.delegate property, and ARC would remove the object from existence immediately after that.
So for each layer, I add a strong #property (strong, nonatomic) delegatesClass *delegatesName and initialise straight to the property. After that, I assign the layer.delegate = self.delegatesName.
This did solve the issue, although I'm not sure if it is the right way to do things.
- (void)fadeOutSplash {
UIImageView *splash = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Default-Landscape~ipad.png"]];
[self.window.rootViewController.view addSubview:splash]; // <-- OBJECT IS BEING RETAINED HERE
[UIView animateWithDuration:0.5
animations:^{
splash.alpha = 0;
}
completion:^(BOOL finished) {
[splash removeFromSuperview];
}];
}
I think ARC is retaining my "splash" when I add it to the subview of the rootViewController. ARC should release "splash" when I run my animation completion because it removes my "splash" from it's own super view. However, I can see in the allocation instruments that this parent view controller is staying allocated and it shows the problem line being where splash is added to the rootViewController. What can I do to make sure "splash" is released?
I fixed this problem, but I'm not exactly sure how.. Here's the likely solution:
- (void)removeFromSuperView
{
// Use this space to manually release any non IB pointers / variables as needed
self.someDictionaryIMadeInInit = nil;
while(self.subviews.count > 0) [[self.subviews objectAtIndex:0] removeFromSuperView];
[super removeFromSuperView];
}
This a little trick I came up with for ARC related views. I recommend it more as a last resort because truly this should be solved the appropriate way, but it's worth a try to save you from tearing out your hair!