CAGradientLayer SetMask on CAShapeLayer crashes app - iphone

Using example code from many great people, I am able to make a complex shape using CAShapeLayer and present that no problem using a subclassed UIView:
#implementation BMTestLogo
+ (Class)layerClass {
return [CAShapeLayer class];
}
- (void)layoutSubviews {
[self setUpPaths];
[self setLayerProperties];
// [self attachAnimations];
}
- (void)setLayerProperties {
CAShapeLayer *layer = (CAShapeLayer *)self.layer;
// _gradient = [CAGradientLayer layer];
CGMutablePathRef combinedPath = CGPathCreateMutableCopy([[_UIBezierPathsArray objectAtIndex:0] CGPath]);
for (UIBezierPath* path in _UIBezierPathsArray) {
CGPathAddPath(combinedPath, NULL, path.CGPath);
}
layer.path = combinedPath;
layer.fillColor = [UIColor clearColor].CGColor;
layer.fillRule = kCAFillRuleEvenOdd;
}
This code works great, now trying to add a CAGradientLayer and mask over the shape, I keep getting a crash, specifically:
QuartzCore CA::Layer::ensure_transaction_recursively(CA::Transaction*):
Here is the Gradient Code, note, this is taken from other, working examples on SO:
#implementation BMViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[_testLogo setNeedsDisplay];
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.startPoint = CGPointMake(0.5,1.0);
gradientLayer.endPoint = CGPointMake(0.5,0.0);
gradientLayer.frame = CGRectMake(CGRectGetMinX(_testLogo.layer.bounds), CGRectGetMinY(_testLogo.layer.bounds), CGRectGetWidth(_testLogo.layer.bounds), CGRectGetHeight(_testLogo.layer.bounds));
NSMutableArray *colors = [NSMutableArray array];
for (int i = 0; i < 2; i++) {
[colors addObject:(__bridge id)[UIColor colorWithHue:(0.1 * i) saturation:1 brightness:.8 alpha:1].CGColor];
}
gradientLayer.colors = colors;
NSNumber *stopOne = [NSNumber numberWithFloat:0.0];
NSNumber *stopTwo = [NSNumber numberWithFloat:1.0];
NSArray *locations = [NSArray arrayWithObjects:stopOne, stopTwo, nil];
gradientLayer.locations = locations;
gradientLayer.needsDisplayOnBoundsChange = YES;
[gradientLayer setMask:_testLogo.layer];
[_testLogo.layer insertSublayer:gradientLayer atIndex:0];
// Do any additional setup after loading the view, typically from a nib.
}
I have tried to debug and research this issue, and the usual suspects of the UIColors not being CGColors or (__bridge id) properly are not there, I am using two locations for the gradient, with only 2 colors, and moreover I have tried dozens of different versions: with or without setNeeds display, properties vs. local declarations, the gradient layer inside the sub class and out, all still giving me the same crash when inserting thesublayer.
When I take out the setMask: call, it does not crash, and just fills the whole frame with the gradient.
This seems like I'm just missing something minor or obvious. Note, using iOS 6 and ARC -- would like to keep using CAShapeLayer and CAGradientLayer as it makes it a bit easier to animate (which is the end goal).

The following code is triggering a stack overflow in ensure_transaction_recursively.
[gradientLayer setMask:_testLogo.layer];
[_testLogo.layer insertSublayer:gradientLayer atIndex:0];
The shape layer has a gradient layer subview which has a shape layer mask which has a gradient layer subview...
Since your goal is to have a gradient layer that is masked by a shape, BMTestLogo should be backed by the CAGradientLayer which then has a the CAShapeLayer as its mask. The shape layer should never reference the gradient layer.

Related

Animating CAGradientLayer

I'm using...
NSArray *colors = [NSArray arrayWithObjects:(id) colorOne.CGColor, colorTwo.CGColor, nil];
CAGradientLayer *headerLayer = [CAGradientLayer layer];
headerLayer.colors = colors;
headerLayer.frame = self.button_editEntry.bounds;
[headerLayer setCornerRadius:10];
[self.button_editEntry.layer insertSublayer:headerLayer
atIndex:0];
... to get a linear fill happening on my button. The problem is when I animate the frame size (using UIView beginAnimations) the CAGradientLayer disappears and doesn't animate with the rest of the frame. Is there a reason this isn't working?
Is there a better way to do linear fades?

UITableView with Transparent Gradient at Top and Bottom

I have searched this forum, Google and other forums and have not found an the answer to my particular issue.
Basically, I have a UIView which contains UITableView. I followed this tutorial and it was partially successful. The problem is the gradient. I have a background image behind the UITableView. So as the cell nears the gradient, I want the background to be showing, instead of the white.
I also found this post which which is where I found the tutorial, but I didn't want to hijack that post with my own questions for matt.
Any help in the right direction would be great!
EDIT1: I know I can use another image with the background image and the middle cut out, but I'm looking for a solution which AVOIDS using PNGs, if possible.
EDIT2: Here's an image of what I get now:
EDIT3:
Here is my code:
Header:
#interface MyView : UIViewController {
CAGradientLayer *_maskLayer;
UITableView *_tableView;
}
#property (nonatomic, retain) CAGradientLayer *maskLayer;
#property (nonatomic, retain) IBOutlet UITableView *tableView;
Implementation:
#implementation HighScoresView_iPhone
#synthesize tableView = _tableView;
#synthesize maskLayer = _maskLayer;
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (![self maskLayer]) {
[self setMaskLayer:[CAGradientLayer layer]];
CGColorRef outerColor = [UIColor colorWithWhite:1.0 alpha:1.0].CGColor;
CGColorRef innerColor = [UIColor colorWithWhite:1.0 alpha:0.0].CGColor;
[[self maskLayer] setColors:[NSArray arrayWithObjects:
(id)outerColor,
(id)innerColor,
(id)innerColor,
(id)outerColor,
nil
]
];
[[self maskLayer] setLocations:[NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0],
[NSNumber numberWithFloat:0.2],
[NSNumber numberWithFloat:0.8],
[NSNumber numberWithFloat:1.0],
nil
]
];
[[self maskLayer] setBounds:CGRectMake(0, 0, [[self scoreTableView] frame].size.width, [[self scoreTableView] frame].size.height)];
[[self maskLayer] setAnchorPoint:CGPointZero];
[[[self scoreTableView] layer] addSublayer:[self maskLayer]];
}
}
You can do this with the CALayer mask property. But you can't set the mask on the table view's own layer because that mask will scroll with the rows of the table. Instead, put your table view inside a new superview (of class UIView) that you create just for this. Call it tableMaskView.
The new superview should have its backgroundColor property set to UIColor.clearColor and its opaque property set to NO. Then you can set its mask like this:
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = self.tableMaskView.bounds;
gradient.colors = [NSArray arrayWithObjects:
(__bridge id)UIColor.clearColor.CGColor,
UIColor.whiteColor.CGColor,
UIColor.whiteColor.CGColor,
UIColor.clearColor.CGColor,
nil];
gradient.locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0],
[NSNumber numberWithFloat:1.0/16],
[NSNumber numberWithFloat:15.0/16],
[NSNumber numberWithFloat:1],
nil];
self.tableMaskView.layer.mask = gradient;
Using a layer mask is not the most efficient way, but it's the easiest to program. Test whether it's fast enough.
Swift 3 version of rob mayoff's answer:
let gradient = CAGradientLayer()
gradient.frame = tableMaskView.bounds
gradient.colors = [UIColor.clear.cgColor, UIColor.white.cgColor,
UIColor.white.cgColor, UIColor.clear.cgColor]
gradient.locations = [0 as NSNumber, 1.0/16.0 as NSNumber,
15.0/16.0 as NSNumber, 1 as NSNumber]
tableMaskView.layer.mask = gradient
Sounds like the background color of your table view is set to white. Try setting the background of your table view to transparent:
tableView.backgroundColor = [ UIColor clearColor ] ;
tableView.opaque = NO ;
Replace this line:
[[[self scoreTableView] layer] addSublayer:[self maskLayer]];
With this one:
self.scoreTableView.layer.mask = self.maskLayer;
(Or if you insist on your syntax-styling even thought I can't see why) :
[[[self scoreTableView] layer] setMask:[self maskLayer]];
Also this answer might help you.

Already have a gradient on my UIView, having trouble inserting a new one

This is the code I already have:
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = backGradient.frame;
gradient.colors = [NSArray arrayWithObjects:(id)[newGradientTop CGColor], (id)[newGradientBottom CGColor], nil];
[backGradient.layer insertSublayer:gradient atIndex:0];
I'm now trying to show a new gradient instead, using the same block of code but different colours going into it. But the new gradient doesn't show, and i think this is because it will not show above the already existing gradient.
How can i fix this?
Use this function (replacing old gradient layer with the new one):
#define kGradientLayerKey #"MyGradientLayer"
void makeViewGradient(UIView *pView,CGColorRef clr1,CGColorRef clr2)
{
CAGradientLayer *gradient = [CAGradientLayer layer];
[gradient setValue:#"1" forKey:kGradientLayerKey];
gradient.frame = pView.bounds;
gradient.colors = [NSArray arrayWithObjects:(id)clr1,(id)clr2,nil];
CALayer *pGradientLayer = nil;
NSArray *ar = pView.layer.sublayers;
for (CALayer *pLayer in ar)
{
if ([pLayer valueForKey:kGradientLayerKey])
{
pGradientLayer = pLayer;
break;
}
}
if (!pGradientLayer) [pView.layer insertSublayer:gradient atIndex:0];
else [pView.layer replaceSublayer:pGradientLayer with:gradient];
pView.backgroundColor = nil;//free memory !
}
Instead of adding additional layers, keep track of your original layer and replace it with your new one. You probably mean to use -replaceSublayer:with: here.
[backGradient.layer insertSublayer:gradient atIndex:0] inserts the new layer at the bottom of the stack, under any other layers. Try
[backGradient.layer addSublayer:gradient] instead.

Fixing CALayer at top and bottom of UIScrollView

I am subclassing UIScrollView and adding some visual elements. At the top, there should be a gradient going from black to clear, and at the bottom there is a mask that fades out towards the bottom. I have those layers added and looking right, but when I scroll, they stay at the coordinates I put them in (with respect to the scroll view), rather than being "fixed" to the bottom and top of the view. This scroll view only scrolls vertically.
Here is the code for SettingsScrollView.m:
#import "SettingsScrollView.h"
#import <QuartzCore/QuartzCore.h>
#define SHADOW_HEIGHT 20.0
#define SHADOW_INVERSE_HEIGHT 10.0
#define SHADOW_RATIO (SHADOW_INVERSE_HEIGHT / SHADOW_HEIGHT)
#implementation SettingsScrollView
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
[self setUpShadow];
return self;
}
- (CAGradientLayer *)shadowAsInverse:(BOOL)inverse
{
CAGradientLayer *newShadow = [[[CAGradientLayer alloc] init] autorelease];
CGRect newShadowFrame = CGRectMake(0, 0, self.frame.size.width, inverse ? SHADOW_INVERSE_HEIGHT : SHADOW_HEIGHT);
newShadow.frame = newShadowFrame;
CGColorRef darkColor =[UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:inverse ? (SHADOW_INVERSE_HEIGHT / SHADOW_HEIGHT) * 0.5 : 0.5].CGColor;
CGColorRef lightColor = [self.backgroundColor colorWithAlphaComponent:0.0].CGColor;
newShadow.colors = [NSArray arrayWithObjects: (id)(inverse ? lightColor : darkColor), (id)(inverse ? darkColor : lightColor), nil];
return newShadow;
}
- (CAGradientLayer *)gradientMask
{
CAGradientLayer *mask = [[[CAGradientLayer alloc] init] autorelease];
CGRect maskFrame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
mask.frame = maskFrame;
CGColorRef darkColor =[UIColor clearColor].CGColor;
CGColorRef lightColor =[UIColor blackColor].CGColor;
mask.colors = [NSArray arrayWithObjects: (id)lightColor, (id)lightColor, (id)darkColor, nil];
mask.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0], [NSNumber numberWithFloat:0.9], [NSNumber numberWithFloat:1.0], nil];
return mask;
}
- (void)setUpShadow
{
CAGradientLayer *topShadowLayer = [self shadowAsInverse:NO];
CAGradientLayer *bottomShadowLayer = [self gradientMask];
[self.layer insertSublayer:topShadowLayer atIndex:0];
[self.layer setMask:bottomShadowLayer];
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
CGRect topShadowLayerFrame = topShadowLayer.frame;
topShadowLayerFrame.size.width = self.frame.size.width;
topShadowLayerFrame.origin.y = 0;
topShadowLayer.frame = topShadowLayerFrame;
CGRect bottomShadowLayerFrame = bottomShadowLayer.frame;
bottomShadowLayerFrame.size.width = self.frame.size.width;
bottomShadowLayerFrame.origin.y = self.frame.size.height - bottomShadowLayer.frame.size.height;
bottomShadowLayer.frame = bottomShadowLayerFrame;
[CATransaction commit];
}
#end
I know one solution for the top could just be to add a separate view that contains a gradient, but for the bottom I believe I need to use a mask to have it do what I want it to (fade out into the background at the very bottom). The background is an image so I can't just fade to white or another color, it needs to fade to clear. I've been looking for a method that gets called when the scroll view is moved and use that to change the position of the mask, but I haven't found anything yet. Any suggestions?
You could probably accomplish this by overriding -layoutSubviews on the UIScrollView, which is invoked when its bounds change.
Another technique I've seen on views like this is that instead of doing this layer management from the view, subclass CALayer, use your subclass as the scroll view's layer, and then in your layer's -layoutSublayers, do to the same work when it moves around. I mention this latter technique, because it seems somewhat more natural for the layer to be managing its sublayers vs the view managing its layers sublayers directly.

shadow effect for UINavigationbar like GameCenter

I want to add shadow effect for UINavigationbar like GameCenter.
I think apply background image with shadow to nav bar, but title's line height would be down.
And I draw shadow to background, but background image would not scroll.
What is the best Practice of this case??
You can subclass UINavigationController and then have a shadow layer for each navigation or if your bar is always visible just add the shadow to UIWindow (only one for the entire application) and then make it the frontmost view each time you add a subview.
CGColorRef darkColor = [[UIColor blackColor] colorWithAlphaComponent:.5f].CGColor;
CGColorRef lightColor = [UIColor clearColor].CGColor;
CAGradientLayer *newShadow = [[[CAGradientLayer alloc] init] autorelease];
newShadow.frame = CGRectMake(0, self.navigationBar.frame.size.height, self.navigationBar.frame.size.width, 10);
newShadow.colors = [NSArray arrayWithObjects:(id)darkColor, (id)lightColor, nil];
[self.navigationBar.layer addSublayer:newShadow];
If you choose the latter case then override the didAddSubview to make the layer the frontmost:
CALayer *superlayer = self.shadowLayer.superlayer;
[self.shadowLayer removeFromSuperlayer];
[superlayer addSublayer:self.shadowLayer];
Hope it helps.
It's easly done with custom subclass of UINavigationController. The key is to overwrite -viewWillAppear: and add sublayer to UINavigationController's view's layer:
- (void)viewWillAppear:(BOOL)animated
{
CGColorRef darkColor = [[UIColor blackColor] colorWithAlphaComponent:.5f].CGColor;
CGColorRef lightColor = [UIColor clearColor].CGColor;
CGFloat navigationBarBottom;
navigationBarBottom = self.navigationBar.frame.origin.y + self.navigationBar.frame.size.height;
CAGradientLayer *newShadow = [[[CAGradientLayer alloc] init] autorelease];
newShadow.frame = CGRectMake(0,navigationBarBottom, self.view.frame.size.width, 10);
newShadow.colors = [NSArray arrayWithObjects:(id)darkColor, (id)lightColor, nil];
[self.view.layer addSublayer:newShadow];
[super viewWillAppear:animated];
}
navigationBarBottom accounts for both UINavigationBar and status bar.
Credit for gradient layer parameters goes to marcio.
Drawing a CAGradientLayer gives a very uniform, but - in my opinion - a rather unnatural looking shadow.
Here's an alternative approach based upon the answer to question UIView with shadow.
It uses the various shadow properties of CALayer to give the shadow effect:
#pragma mark - View lifecycle
- (void)viewDidLoad
{
    [super viewDidLoad];
    
self.navigationController.navigationBar.layer.shadowColor = [[UIColor blackColor] CGColor];
self.navigationController.navigationBar.layer.shadowOffset = CGSizeMake(0.0f,0.0f);
self.navigationController.navigationBar.layer.shadowOpacity = 1.0f;
self.navigationController.navigationBar.layer.shadowRadius = 4.0f;
}
The same technique works with the tabBarController's tabBar;
self.tabBarController.tabBar.layer.shadowColor = [[UIColor blackColor] CGColor];
self.tabBarController.tabBar.layer.shadowOffset = CGSizeMake(0.0f,0.0f);
self.tabBarController.tabBar.layer.shadowOpacity = 1.0f;
self.tabBarController.tabBar.layer.shadowRadius = 4.0f;