Trying to remove a sublayer causes terminating with uncaught exception of type NSException - objective-c

I'm adding a gradient layer and attaching name to it.
Later on when I try to remove it by searching for that name, my app crashes. Here's the code I'm using.
CAGradientLayer* gradient = [CAGradientLayer layer];
gradient.colors = [NSArray arrayWithObjects:
..... setting up gradient.....
gradientLayer.name = #"GradientLayer";
[self.myView.layer insertSublayer:gradient atIndex:0];
Later on I'm trying to remove it.
for (CALayer *layer in self.myView.layer.sublayers) {
if ([layer.name isEqualToString:#"GradientLayer"])
{
[layer removeFromSuperlayer];
}
}
It crashes when it tries to remove the sublayer.

Your loop mutates self.myView.layer.sublayers while it is being enumerated.
Add break; after [layer removeFromSuperLayer]; to stop enumerating, then you will be fine.

Related

Custom Spinner class with rotation animation problem

I programmed my own view containing an imageview which should be rotating. Here is my rotation animation:
- (void)startPropeller
{
//_movablePropeller = [[UIImageView alloc] initWithFrame:self.frame];
//_movablePropeller.image = [UIImage imageNamed:#"MovablePropeller"];
//[self addSubview:self.movablePropeller];
self.hidden = NO;
CABasicAnimation *rotation;
rotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
rotation.fromValue = [NSNumber numberWithFloat:0.0f];
rotation.toValue = [NSNumber numberWithFloat:(2 * M_PI)];
rotation.cumulative = true;
rotation.duration = 1.2f; // Speed
rotation.repeatCount = INFINITY; // Repeat forever. Can be a finite number.
[self.movablePropeller.layer removeAllAnimations];
[self.movablePropeller.layer addAnimation:rotation forKey:#"Spin"];
}
And here is how I start it:
self.loadingPropeller = [[FMLoadingPropeller alloc] initWithFrame:self.view.frame andStyle:LoadingPropellerStyleNoBackground];
self.loadingPropeller.center=self.view.center;
[self.view addSubview:self.loadingPropeller];
[self.loadingPropeller startPropeller];
Problem is: Without any further code. The propeller is not rotating. So I was able to solve it by adding this code into my class implementing to rotating propeller spinner:
-(void)viewDidAppear:(BOOL)animated
{
if(!self.loadingPropeller.hidden){
[self.loadingPropeller startPropeller];
}
}
But I don't like that too much. Isn't it possible to add some code within the Propeller class to solve this issue automatically, without having to add also code in every class in the viewDidAppear method?
The code that doesn't work does two essential things: adding the spinner to the view hierarchy and positioning it. My guess is that the failure is due to positioning it before layout has happened. Try this:
// in viewDidLoad of the containing vc...
self.loadingPropeller = [[FMLoadingPropeller alloc] initWithFrame:CGRectZero andStyle:LoadingPropellerStyleNoBackground];
[self.view addSubview:self.loadingPropeller];
// within or after viewDidLayoutSubviews...
// (make sure to call super for any of these hooks)
self.loadingPropeller.frame = self.view.bounds;
self.loadingPropeller.center = self.view.center;
// within or after viewDidAppear (as you have it)...
[self.loadingPropeller startPropeller];

removeFromSuperlayer not removing layer

I am writing code that will allow the user to temporarily highlight a section of an image. In my view class, I used touchesBegan, touchesMoved and, touchesEnded to pick up a UIBezierPath from the screen. I added the path to a layer, stroked the path and used a animation to fade the opacity of the layer from 1 to 0. The two NSLog statements confirm that the layer is added to the sublayer array
drawVanishingPath
{
NSLog(#"There were %d sublayers before the path was added",[self.layer.sublayers count]);
disappearingLayer = [[CAShapeLayer alloc] init];
disappearingLayer.strokeColor = self.strokeColor.CGColor;
disappearingLayer.fillColor = [UIColor clearColor].CGColor;
disappearingLayer.lineWidth = [self.strokeSize floatValue];
disappearingLayer.path = path.CGPath;
[self.layer addSublayer:disappearingLayer];
[disappearingLayer addAnimation:fadeAnimation forKey:#"opacity"];
[fadeAnimation setValue:disappearingLayer forKey:#"parentLayer"];
disappearingLayer.opacity = 0.0;
NSLog(#"There are %d sublayers after adding the path",[self.layer.sublayers count]);
}
Based on the answer to another question on stack overflow (How to remove a layer when its animation completes?) I set the delegate for the animation and implemented animationDidStop: finished: as shown below. I added two NSLog statements to confirm that the layer was removed from the layer array.
-(void)animationDidStop:(CAAnimation *)animation finished:(BOOL)flag
{
NSLog(#"There were %d sublayers",[self.layer.sublayers count]);
CAShapeLayer *layer = [animation valueForKey:#"parentLayer"];
[layer removeAllAnimations];
[layer removeFromSuperlayer];
NSLog(#"There are now %d sublayers",[self.layer.sublayers count]);
}
When the program runs, the layer is added and the layer count is incremented as expected, but the layer count does not decrement in animationDidStop: finished:. Since the layers are not removed, the program will have many unneeded layers in the program. These could cause problems later on.
I believe I am misunderstanding something, but I am not sure what is wrong. Any suggestions would be appreciated.
How to remove a layer when its animation completes?
You're on the right track! The problem is these lines:
[disappearingLayer addAnimation:fadeAnimation forKey:#"opacity"];
[fadeAnimation setValue:disappearingLayer forKey:#"parentLayer"];
They are in the wrong order! Reverse their order and all will be well.
The reason: you can't modify the animation after adding it to a layer. Well, you can, but it does no good: the animation has been copied, so what you're modifying is now not the animation you added.
Thus, you were never setting the animation's parentLayer key. So in the delegate method, that key was nil and no layer was being removed.
As a test, I ran this simplified version of your code, and it worked as expected:
- (void)drawVanishingPath {
NSLog(#"There were %d sublayers before the path was added",[self.layer.sublayers count]);
CAShapeLayer* disappearingLayer = [[CAShapeLayer alloc] init];
disappearingLayer.strokeColor = [UIColor redColor].CGColor;
disappearingLayer.fillColor = [UIColor clearColor].CGColor;
disappearingLayer.lineWidth = 5;
disappearingLayer.path = _path;
[self.layer addSublayer:disappearingLayer];
CABasicAnimation* fadeAnimation = [CABasicAnimation animationWithKeyPath:#"opacity"];
fadeAnimation.toValue = #0;
fadeAnimation.duration = 2;
fadeAnimation.delegate = self;
[fadeAnimation setValue:disappearingLayer forKey:#"parentLayer"];
[disappearingLayer addAnimation:fadeAnimation forKey:#"opacity"];
NSLog(#"There are %d sublayers after adding the path",[self.layer.sublayers count]);
}
-(void)animationDidStop:(CAAnimation *)animation finished:(BOOL)flag {
NSLog(#"There were %d sublayers",[self.layer.sublayers count]);
CAShapeLayer *layer = [animation valueForKey:#"parentLayer"];
[layer removeAllAnimations];
[layer removeFromSuperlayer];
NSLog(#"There are now %d sublayers",[self.layer.sublayers count]);
}
The log reads:
2014-05-03 17:23:21.204 PathTest[5100:60b] There were 0 sublayers before the path was added
2014-05-03 17:23:21.209 PathTest[5100:60b] There are 1 sublayers after adding the path
2014-05-03 17:23:23.210 PathTest[5100:60b] There were 1 sublayers
2014-05-03 17:23:23.211 PathTest[5100:60b] There are now 0 sublayers

Unrecognized Selector sent to Instance in another class

I've been trying to figure out something that might be obvious and I'm missing the point or losing it. Is something wrong with my toggleStormButtons function that XCode isn't seeing it?
In my main class I have the following to call a function in another class:
STopLeftMenu *mTopLeft = [[STopLeftMenu alloc]init];
[mTopLeft drawStormToggleButton];
Then in the other class I have 2 functions:
- (void)toggleStormButtons{
[UIButton animateWithDuration:0.50 animations:^{
if (stormToggleBtn.transform.tx == 0){
[stormToggleBtn setTransform:CGAffineTransformMakeTranslation(307, 0)];
UIImage* hideButtonImg = [UIImage imageNamed:#"aiga_right_arrow_mod_hide_resize.png"];
[stormToggleBtn setBackgroundImage:hideButtonImg forState:UIControlStateNormal];
}
else{
[stormToggleBtn setTransform:CGAffineTransformMakeTranslation(0, 0)];
UIImage* showButtonImg = [UIImage imageNamed:#"aiga_right_arrow_mod_show_resize.png"];
[stormToggleBtn setBackgroundImage:showButtonImg forState:UIControlStateNormal];
}
}];
for(UIView* storm in stormButtonSaves){
[UIView animateWithDuration:0.50 animations:^{
if (storm.transform.tx == 0){
[storm setTransform:CGAffineTransformMakeTranslation(307, 0)];
storm.alpha = .65;
}
else{
[storm setTransform:CGAffineTransformMakeTranslation(0, 0)];
storm.alpha = 0;
}
}];
}
}
- (void)drawStormToggleButton{
//Storm Pullout Toggle Button
UIImage *buttonImageNormal = [UIImage imageNamed:#"aiga_right_arrow_mod_show_resize.png"];
stormToggleBtn = [[UIButton alloc] initWithFrame:CGRectMake(10, 10, 65, 75) ];
[stormToggleBtn setBackgroundImage:buttonImageNormal forState:UIControlStateNormal];
stormToggleBtn.backgroundColor = [UIColor clearColor];
stormToggleBtn.alpha = 0.5;
[stormToggleBtn addTarget:self action:#selector(toggleStormButtons) forControlEvents:UIControlEventTouchUpInside];
[viewsToRemove addObject:stormToggleBtn];
[mv addSubview:stormToggleBtn];
}
I seem to be getting an unrecognized selector message:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSMallocBlock__ toggleStormButtons]: unrecognized selector sent to instance 0x7bc9ca0'
*** First throw call stack:
(0x1b9e012 0x1966e7e 0x1c294bd 0x1b8dbbc 0x1b8d94e 0x197a705 0x8ae2c0 0x8ae258 0x96f021 0x96f57f 0x96e6e8 0x8ddcef 0x8ddf02 0x8bbd4a 0x8ad698 0x2599df9 0x2599ad0 0x1b13bf5 0x1b13962 0x1b44bb6 0x1b43f44 0x1b43e1b 0x25987e3 0x2598668 0x8aaffc 0x2285 0x2185)
libc++abi.dylib: terminate called throwing an exception
It sounds like your STopLeftMenu is being deallocated too early. The button does not retain its target, so you'll need to keep this object around as long as it needs to respond to the button's messages. If you're not sure how the object is getting deallocated, try debugging with Instruments.
I don't see anything wrong with the code you have shown. I tried it in an existing app of mine and while I had to a) declare a UIButton local variable, b) change the image used for the button and c) comment out the stuff in toggleStormButtons, the method was called every time I tapped the button, no problems.
You don't show your storage for the button. Are you using ARC? Is the button strong? If ARC it should be strong. If not ARC and you are not using a property to assign with a retain, that could cause problems.
What does viewsToRemove do? Looks like an array but it could be something else.
Why don't you use + buttonWithType: and set the frame later?

In my custom UIButton class, why is my highlight layer not vanishing?

I've created a custom subclass of UIButton to encapsulate some specific issues I have (client wants to have a gradient as the background for most of his buttons). So far, this effort has worked as planned.
However, one of the things I created was a custom 'highlight' to replace the built-in highlight, since custom buttons aren't allowed that. I've had great luck with overriding the superclass function for highlighting as follows:
-(void) setHighlighted:(BOOL)highlighted
{
[super setHighlighted:highlighted];
NSLog(#"Set highlighted called: %i", highlighted);
if (self.highlight==nil) {
self.highlight=[CAGradientLayer layer];
gradient.colors=[NSArray arrayWithObjects:
(id)[[UIColor colorWithWhite:1.0 alpha:.4] CGColor],
(id)[[UIColor colorWithWhite:0.0 alpha:.4] CGColor],
nil];
}
if(highlighted)
{
[[self layer] addSublayer:highlight];
}
else {
NSLog(#"Highlight should be removed...");
NSLog(#"Highlight index: %i", [[[self layer] sublayers] indexOfObject:highlight]);
[self.highlight removeFromSuperlayer];
NSLog(#"Highlight index: %i", [[[self layer] sublayers] indexOfObject:highlight]);
}
}
Unfortunately, the highlight layer doesn't seem to remove itself properly. As you can see in my code, I'm checking to see what index the highlight is at before removal (2) and after removal (2147483647, AKA NSNotFound). So it is, in theory, removed from the layer.
But for some reason, it's still being displayed inside the custom button, and I don't know why.
For now I"m going to work around by simply changing the background gradient's colors on highlight, but I'm really curious as to why this code isn't working.

Clear a CALayer

I use a CALayer and CATextLayers to lay out the numbers on a sudoku grid on the iPhone.
I have a tableView that lists some sudokus. When I tap one table cell it shows the sudoku in another viewController that is pushed on to the navigation controller.
In my - (void)viewWillAppear... method I call my - (void)loadSudoku method which I will show you below.
The problem is when you look at one sudoku, go back to the table view using the "back" button in the navigationBar and then tap another sudoku. Then the old sudoku is still there, and the new one is drawn on top of the old one.
I guess I need to clear the old one somehow. Any ideas?
I do have a background image set through the interface builder of the actual sudoku grid. I don't want to remove this.
The method that draws the sudoku looks like this:
- (void)loadSudoku
{
mainLayer = [[self view] layer];
[mainLayer setRasterizationScale:[[UIScreen mainScreen] scale]];
int col=0;
int row=0;
for(NSNumber *nr in [[self sudoku] sudoku])
{
if([nr intValue] != 0)
{
//Print numbers on grid
CATextLayer *messageLayer = [CATextLayer layer];
[messageLayer setForegroundColor:[[UIColor blackColor] CGColor]];
[messageLayer setContentsScale:[[UIScreen mainScreen] scale]];
[messageLayer setFrame:CGRectMake(col*36+5, row*42, 30, 30)];
[messageLayer setString:(id)[nr stringValue]];
[mainLayer addSublayer:messageLayer];
}
if(col==8)
{
col=0; row++;
}else
{
col++;
}
}
[mainLayer setShouldRasterize:YES];
}
To remove only text layers, you can do this –
NSIndexSet *indexSet = [mainLayer.sublayers indexesOfObjectsPassingTest:^(id obj, NSUInteger idx, BOOL *stop){
return [obj isMemberOfClass:[CATextLayer class]];
}];
NSArray *textLayers = [mainLayer.sublayers objectsAtIndexes:indexSet];
for (CATextLayer *textLayer in textLayers) {
[textLayer removeFromSuperlayer];
}
In a nutshell, the first statement gets all the indices of text layers which are a sublayer to over root layer. Then in the second statement we get all those layers in a separate array and then we remove them from their superlayer which is our root layer.
Original Answer
Try doing this,
mainLayer.sublayers = nil;