Adding subview to CollectionViewCell just once (cell reuseability issues) - ios

I would like to add a simple gradient at the bottom of each cell. In cellForItemAtIndexPath: I have the code:
CAGradientLayer *bottomFade = [CAGradientLayer layer];
bottomFade.name = #"Gradient";
bottomFade.frame = CGRectMake(0, cell.background.frame.size.height*0.8, cell.background.frame.size.width, cell.background.frame.size.height*0.2);
bottomFade.endPoint = CGPointMake(0.5, 1.0);
bottomFade.startPoint = CGPointMake(0.5, 0.0);
bottomFade.colors = [NSArray arrayWithObjects:(id)[[UIColor colorWithWhite:0.0 alpha:0.0f] CGColor], (id)[[UIColor colorWithWhite:0.0 alpha:0.2f] CGColor], nil];
[cell.background.layer addSublayer:bottomFade];
The problem is that when the cell is being scrolled, the sublayer is added over and over again, which obviously is not a desired effect.
I know how to handle reuseability issues when it comes to the UITableView, but what should I do when it comes to working with the UICollectionView?
I tried to set a custom cell property isConfigured, but when I checked for it in cellForItemAtIndexPath: the result was that only two cells were generated, and then they were repeating (honestly, I have absolutely no idea why).
What would you suggest to handle such a problem? Perhaps it would be better to add some custom code in the cell's subclass?

Put your code that only requires once inside 'awakeFromNib' so it won't be called twice or thrice on its life cycle
which would appear like this:
- (void)awakeFromNib {
[super awakeFromNib];
CAGradientLayer *bottomFade = [CAGradientLayer layer];
bottomFade.name = #"Gradient";
bottomFade.frame = CGRectMake(0, self.background.frame.size.height*0.8, self.background.frame.size.width, self.background.frame.size.height*0.2);
bottomFade.endPoint = CGPointMake(0.5, 1.0);
bottomFade.startPoint = CGPointMake(0.5, 0.0);
bottomFade.colors = [NSArray arrayWithObjects:(id)[[UIColor colorWithWhite:0.0 alpha:0.0f] CGColor], (id)[[UIColor colorWithWhite:0.0 alpha:0.2f] CGColor], nil];
[self.background.layer addSublayer:bottomFade];
}

I've already solved the problem ;).
In the cell's subclass:
-(void)layoutSubviews {
if (!self.isConfigured) {
CAGradientLayer *bottomFade = [CAGradientLayer layer];
bottomFade.name = #"Gradient";
bottomFade.frame = CGRectMake(0, self.background.frame.size.height*0.8, self.background.frame.size.width, self.background.frame.size.height*0.2);
bottomFade.endPoint = CGPointMake(0.5, 1.0);
bottomFade.startPoint = CGPointMake(0.5, 0.0);
bottomFade.colors = [NSArray arrayWithObjects:(id)[[UIColor colorWithWhite:0.0 alpha:0.0f] CGColor], (id)[[UIColor colorWithWhite:0.0 alpha:0.2f] CGColor], nil];
[self.background.layer addSublayer:bottomFade];
self.isConfigured = YES;
}
}
That does the trick just fine! However, I'm still quite curious about whether there's an easy fix for the problem in cellForItemAtIndexPath: or not.

The cellForItemAtIndexPath: method will be called every time the UICollectionView instance create or reuse a UICollectionViewCell, that's where the problem is.
I don't recommend you use the property like isConfigured, the cell should keep a BOOL variable, and will check the property every time layoutSubviews been called, its a waste of memory and performance.
As nferocious76 said, awakeFromNib: method, which will be called only once on its life cycle, is the best solution for this case.
By the way, as I can't comment, you should call [super layoutSubviews] at the begin of layoutSubviews method.

Related

UICollectionViewCell strange behaviour. Cells not updating layer correctly

I have a UICollectionView with multiple cells on which I add a CAGradient layer representing the color of each cell. The problem is that when I push another view controller on top of the present view controller and then pop the second view controller, my collection view cells shift colors in a random order. To give you an idea I have attached screenshots.
This is the original order of the cells. This is correct
This happens when I push another view controller and then return
You can see that the cells shifted their colors even though I changed nothing.
This is the code I use to initialize the cells.
[collectionview reloadData] is called in -viewWillAppear so the cells load every time the view appears
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
[self filterRecords];
MyCell *cell = (MyCell*)[collectionView dequeueReusableCellWithReuseIdentifier:#"ProspectCVCell" forIndexPath:indexPath];
for (int i = 0; i < [eventsSortedForAddedDate count]; i++)
{
Events* event = (Events*)[eventsSortedForAddedDate objectAtIndex:i];
if ([event.activityLevel.activityName isEqualToString:#"None"])
continue;
color = [[event activityLevel] color];
if (![color isEqualToString:#"#FFCCCCCC"])
break;
else
color = nil;
}
if (!([color length] > 0) || color == nil)
{
color = #"#FFCCCCCC";
}
UIColor* myColor = [self getUIColorObjectFromHexString:color alpha:.9];
//cell.backgroundColor = myColor;
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = cell.bounds;
gradient.colors = [NSArray arrayWithObjects:(id)[myColor CGColor], (id)[[UIColor whiteColor] CGColor], nil];
gradient.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0f], [NSNumber numberWithFloat:0.85f], nil];
//[cell.layer insertSublayer:gradient atIndex:0];
[cell.layer insertSublayer:gradient atIndex:0];
cell.prospectImageView.layer.shadowColor = [UIColor blackColor].CGColor;
cell.prospectImageView.layer.shadowOffset = CGSizeMake(0, 3.0);
cell.prospectImageView.layer.shadowRadius = 3.0;
cell.prospectImageView.layer.shadowOpacity = 0.8;
cell.cellLabel.text = p.displayName;
cell.layer.borderWidth = 0.5f;
cell.layer.borderColor = [UIColor whiteColor].CGColor;
return cell;
}
There is nothing wrong in the way I get the color, I have debugged multiple times and checked that the colors I get are the correct ones.
If I do
cell.backgroundColor = myColor;
The cells do not change their colors and function as expected. So I am pretty sure that the problem lies with the CAGradientLayer.
I have tried everything that I could think of but nothing seems to work!
Try this once,
[cell.layer insertSublayer:gradient atIndex:[[cell.layer sublayers] indexOfObject:[[cell.layer sublayers] lastObject]]];
instead of
[cell.layer insertSublayer:gradient atIndex:0];
Update :
As asked in comment, Apple doc states about dequeueReusableCellWithReuseIdentifier,
Call this method from your data source object when asked to provide a new cell for the collection view. This method dequeues an existing cell if one is available or creates a new one based on the class or nib file you previously registered.
And second thing you can also remove previous layers and then can add new one at index 0. it is better because it not increase number of layer which is not necessary.

Shadow effects on ImageView in iOS

I am trying to provide a shadow effect to my Imageview just like in this image.
but the problem which I am facing it is that the shadow is actually visible from the bottom of the Imageview.
Here is the code which I have done to add the shadow. The color and all is still not matching with this one.
CAGradientLayer *shadow = [CAGradientLayer layer];
shadow.frame = CGRectMake(-100, 70, perspectiveView.frame.size.width,
perspectiveView.frame.size.height - 20);
shadow.startPoint = CGPointMake(1.0, 0.5);
shadow.endPoint = CGPointMake(0, 0.5);
shadow.colors = [NSArray arrayWithObjects:(id)[[UIColor colorWithWhite:0.0
alpha:0.4f] CGColor], (id)[[UIColor clearColor] CGColor], nil];
shadow.opacity = 1.0f;
[perspectiveView.layer addSublayer:shadow];
Please provide inputs. Also if the approach is wrong, feel free to guide me to any other approach.
Thanks in advance.
Also can anyone suggest how to provide a 3D border just like in the image which provides a slight width to the image view?
Perhaps, adding a shadow to the layer seems to work. You may want to try something like this:
// perspectiveView.transform = CGAffineTransformMakeRotation(-50.0f);
perspectiveView.layer.shadowOffset = CGSizeMake(10, 10);
perspectiveView.layer.shadowRadius = 5.0;
perspectiveView.layer.shadowOpacity = 0.6;
perspectiveView.layer.masksToBounds = NO;
You will need to playaround with these values to match your requirements. Hope this helps.
Here is my output:
Try this,
- (void)setShadow {
[perspectiveView.layer setShadowOffset:CGSizeMake(-5.0, 5.0)];
[perspectiveView.layer setShadowRadius:5.0];
[perspectiveView.layer setShadowOpacity:1.0];
}
you can use CALayer class , the layer can manage your visual aspects like background color, border, shadow and with it you can also manage the geometry of it content like size , transform etc .
visual effect(shadowing)
self.yourView.layer.shadowOffset = CGSizeMake(0, 3);
self.yourView.layer.shadowRadius = 5.0;
self.yourView.layer.shadowColor = [UIColor blackColor].CGColor;
self.yourView.layer.shadowOpacity = 1;
and you also used it for like self.yourView.layer.frame(Geometry)
more info https://developer.apple.com/library/ios/documentation/graphicsimaging/reference/CALayer_class/index.html
Below code work i have checked properly.
but Shikhar varshney can u check setShadow method call and perspectiveView is not nil.
- (void)setShadow {
perspectiveView.layer.shadowColor = [UIColor colorWithWhite:0.0
alpha:0.4f] CGColor;
perspectiveView.layer.shadowOffset = CGSizeMake(0, 1);
perspectiveView.layer.shadowOpacity = 1;
perspectiveView.layer.shadowRadius = 1.0;
perspectiveView.clipsToBounds = NO;
}

Change UIButton gradient color

I use the following method to add some gradient effect to my IOS UIButton. I call it in viewController's viewDidLoad, works great, so far so good.
Now, I'd like to change the color of the button according to some user interaction. What I do is simply calling this method by different colors (fromColor, toColor) as input. The problem, the look of my button does not change. I tried to call setNeedsDisplay, but did not help.
Would you be so kind to help me? What I miss? There is should be some problem with layers, should be rested or something like that, but I could not find its proper way.
Update: By using setSubLayers instead of insertSubLayer color changes, but button title disappears.
+(void)setCustomButtonStyle:(UIButton *)button cornerRadius:(float) cornerRadius fromColor:(UIColor *)fromColor toColor:(UIColor *)toColor normalTitleColor:(UIColor *)normalTitleColor highlightedTitleColor:(UIColor *)highlightedTitleColor borderWidth:(float) borderWidth borderColor:(UIColor*)borderColor
{
[button setTitleColor:normalTitleColor forState:UIControlStateNormal];
[button setTitleColor:highlightedTitleColor forState:UIControlStateHighlighted];
CAGradientLayer *btnGradient = [CAGradientLayer layer];
btnGradient.frame = button.bounds;
btnGradient.colors = [NSArray arrayWithObjects:
(id)[fromColor CGColor],
(id)[toColor CGColor],
nil];
btnGradient.borderWidth = borderWidth;
btnGradient.borderColor = [borderColor CGColor];
[button.layer insertSublayer:btnGradient atIndex:0];
CALayer *btnLayer = [button layer];
[btnLayer setMasksToBounds:YES];
[btnLayer setCornerRadius:cornerRadius];
}
The problem you are having here is that 1. you are creating a new layer each time you are changing the colour and 2. you are then placing that new layer underneath everything else so you can't see it anyway. (Think of putting a playing card at the bottom of the deck).
What you need to do is keep hold of the gradient layer and then update then instead of creating a new one.
You are probably best doing this in a category on UIButton.
Something like... UIButton+GradientBackground or something.
In the .h file give it a single function...
// Use CGFloat not float. Also user NSInteger not int.
// Always.
// Also, I have used modern Obj-C syntax. Use this too.
- (void)setCornerRadius:(CGFloat)cornerRadius
fromColor:(UIColor *)fromColor
toColor:(UIColor *)toColor
normalTitleColor:(UIColor *)normalTitleColor
highlightedTitleColor:(UIColor *)highlightedTitleColor
borderWidth:(CGFloat)borderWidth
borderColor:(UIColor *)borderColor;
Then in the .m file you need to also give it a property...
#property (nonatomic, strong) CAGradientLayer *gradientLayer;
Now in the function...
- (void)setCornerRadius:(CGFloat)cornerRadius
fromColor:(UIColor *)fromColor
toColor:(UIColor *)toColor
normalTitleColor:(UIColor *)normalTitleColor
highlightedTitleColor:(UIColor *)highlightedTitleColor
borderWidth:(CGFloat)borderWidth
borderColor:(UIColor *)borderColor
{
[self setTitleColor:normalTitleColor forState:UIControlStateNormal];
[self setTitleColor:highlightedTitleColor forState:UIControlStateHighlighted];
if (!self.grandientLayer) {
// if it doesn't exist then create it and add it (only once)
self.gradientLayer = [CAGradientLayer layer];
[self.layer insertSublayer:self.gradientLayer atIndex:0];
}
self.gradientLayer.frame = self.bounds;
self.gradientLayer.colors = #[(id)fromColor.CGColor, (id)toColor.CGColor]; // use modern syntax
self.gradientLayer.borderWidth = borderWidth;
self.gradientLayer.borderColor = borderColor.CGColor;
[self.layer setMasksToBounds:YES];
[self.layer setCornerRadius:cornerRadius];
}
Notice that I have removed the button from the function name as this function will essentially be part of that UIButton's functions so self is the button you are changing.
You would call it like this...
[someButton setCornerRadius:5 fromColor:[UIColor redColor] toColor... and so on];
By doing this you only create that gradient layer once and you keep hold of it so that you can update it the next time round.
For first time add gradient layer, next time replace added gradient layer
Added Category as #Fogmeister thinks this can be used with category only
#interface UIButton (gradient)
+(void)setCustomButtonStyle:(UIButton *)button cornerRadius:(float) cornerRadius fromColor:(UIColor *)fromColor toColor:(UIColor *)toColor normalTitleColor:(UIColor *)normalTitleColor highlightedTitleColor:(UIColor *)highlightedTitleColor borderWidth:(float) borderWidth borderColor:(UIColor*)borderColor;
#end
#implementation UIButton (gradient)
+(void)setCustomButtonStyle:(UIButton *)button cornerRadius:(float) cornerRadius fromColor:(UIColor *)fromColor toColor:(UIColor *)toColor normalTitleColor:(UIColor *)normalTitleColor highlightedTitleColor:(UIColor *)highlightedTitleColor borderWidth:(float) borderWidth borderColor:(UIColor*)borderColor
{
//Set Title color as you want
[button setTitleColor:normalTitleColor forState:UIControlStateNormal];
[button setTitleColor:highlightedTitleColor forState:UIControlStateHighlighted];
//check gradientlayer exist
id layer = nil;
if (button.layer.sublayers.count > 0) {
layer = [button.layer.sublayers objectAtIndex:0];
}
//Find added gradient layer
CAGradientLayer *addedGradLayer = nil;
if (layer) {
if ([layer isKindOfClass:[CAGradientLayer class]]) {
addedGradLayer = (CAGradientLayer *)[button.layer.sublayers objectAtIndex:0];
}
}
//check gradient layer exists
if (!addedGradLayer) //first time
{
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = button.layer.bounds;
gradientLayer.colors = [NSArray arrayWithObjects:
(id)fromColor.CGColor,
(id)toColor.CGColor,
nil];
gradientLayer.locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0f],
[NSNumber numberWithFloat:1.0f],
nil];
gradientLayer.cornerRadius = button.layer.cornerRadius;
[button.layer insertSublayer:gradientLayer atIndex:0];
button.clipsToBounds = YES;
}
else //next time
{
CAGradientLayer *newGradientLayer = [CAGradientLayer layer];
newGradientLayer.frame = button.layer.bounds;
newGradientLayer.colors = [NSArray arrayWithObjects:
(id)fromColor.CGColor,
(id)toColor.CGColor,
nil];
newGradientLayer.locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0f],
[NSNumber numberWithFloat:1.0f],
nil];
newGradientLayer.cornerRadius = button.layer.cornerRadius;
[button.layer replaceSublayer:addedGradLayer with:newGradientLayer];
}
}
#end

Grandient background for a view on iOS

I'm pretty new to iOS developpement, and I'm trying to set my view background to a gradient.
I created a CAGradientLayer and try to set it. It goes throught building, but my app crashes as soon as it open, throwing a "EXC_BAD_ACCESS".
Here is my code:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
CAGradientLayer *bgLayer = [CAGradientLayer layer];
UIColor *grey = [UIColor colorWithRed:(255/255.0) green:(255/255.0) blue:(255/255.0) alpha:1.0];
UIColor *black = [UIColor colorWithRed:(180/255.0) green:(180/255.0) blue:(180/255.0) alpha:1.0];
NSNumber *top = [NSNumber numberWithFloat:0.0];
NSNumber *bot = [NSNumber numberWithFloat:1.0];
bgLayer.colors = [NSArray arrayWithObjects:grey, black, nil];
bgLayer.locations = [NSArray arrayWithObjects:top, bot, nil];
bgLayer.frame = self.view.bounds;
[self.view.layer addSublayer:bgLayer];
}
I think I understood that it comes from a bad memory management, but don't what I did wrong. Thanks for your answers in advance.
Note what the docs say about the colors:
An array of CGColorRef objects defining the color of each gradient stop.

UIView animation duration

I have an UIView and I want to change it's background color. That works well using the UIView beginAnimation:context: method. However, the animation 'lasts' for like 1 second or so. I want it to last 5 seconds. This is the code I'm using:
- (void)updateSky:(NSString *)time {
[UIView beginAnimations: #"backgroundUpdate" context:nil];
[UIView setAnimationDuration: 5.0];
gradient.colors = [NSArray arrayWithObjects: (id)[[UIColor colorWithRed:0x63/255.0f green:0xA9/255.0f blue:0xFF/255.0f alpha:1.0] CGColor],
(id)[[UIColor colorWithRed:0x8C/255.0f green:0xBF/255.0f blue:0xFF/255.0f alpha:1.0] CGColor], nil];
if(time == #"night") {
gradient.colors = [NSArray arrayWithObjects: (id)[[UIColor colorWithRed:0x1E/255.0f green:0x1E/255.0f blue:0x1E/255.0f alpha:1.0] CGColor],
(id)[[UIColor colorWithRed:0x2C/255.0f green:0x4C/255.0f blue:0x72/255.0f alpha:1.0] CGColor], nil];
}
[UIView commitAnimations];
}
gradient is defined as follows:
CAGradientLayer *gradient;
Am I doing something wrong?
The way to animate setting a UIView's background color using view animation is to set its backgroundColor property. You don't seem to be doing that. What is this thing gradient and what is its colors property? The problem here seems to be that you're using UIView animation, but colors is not a UIView animatable property so this makes no sense. You need to provide more code to show what you're really doing. For example, if this is an implicit layer animation you'd use +[CATransaction setAnimationDuration:] to set the duration.
(Also this whole beginAnimations / commitAnimations structure, while not exactly deprecated, is virtually on the chopping block. That's irrelevant to your question, but it would be good to get out of it if you can, just on principle. You should be using animateWithDuration:delay:options:animations:completion:, or implicit layer animation, or explicit Core Animation.)

Resources