I have my very own minimal view class with this:
- (void) awakeFromNib
{
NSLog(#"awakeFromNib!");
[self.layer setDelegate:self];
[self.layer setFrame:CGRectMake(30, 30, 250, 250)];
self.layer.masksToBounds = YES;
self.layer.cornerRadius = 5.0;
self.layer.backgroundColor = [[UIColor redColor] CGColor];
[self setNeedsDisplay];
}
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
NSLog(#"drawing!");
}
drawLayer:inContext never get called, although I can see the layer as red, rounded corner rectangle. What am I missing?
EDIT: from Apple docs
You can draw content for your layer,
or better encapsulate setting the
layer’s content image by creating a
delegate class that implements one of
the following methods: displayLayer:
or drawLayer:inContext:.
Implementing a delegate method to draw
the content does not automatically
cause the layer to draw using that
implementation. Instead, you must
explicitly tell a layer instance to
re-cache the content, either by
sending it a setNeedsDisplay or
setNeedsDisplayInRect: message, or by
setting its needsDisplayOnBoundsChange
property to YES.
Also
drawLayer:inContext:
If defined, called by the default implementation
of drawInContext:.
You should never change the delegate of layer of a UIView. From documentation of UIView layer property:
Warning: Since the view is the layer’s
delegate, you should never set the
view as a delegate of another CALayer
object. Additionally, you should never
change the delegate of this layer.
If you want to do custom drawing in a view simply override the drawRect: method.
If you do want to use layers you need to create your own:
UIView *myView = ...
CALayer *myLayer = [CALayer layer];
myLayer.delegate = self;
[myView.layer addSublayer:myLayer];
In both cases you need to call setNeedsDisplay on the view in the first case and on your custom layer in the second. You never call drawRect: or drawLayer:inContext: directly, they are called automatically when you call setNeedsDisplay.
Use
[self.layer setNeedsDisplay];
instead of
[self setNeedsDisplay];
It is the view.layer, not the view
Related
I'm in a dilemma which method to use for setting frames of custom UIViews with many subviews in it and still have animations and automatically adjust to rotations. What I usually do when I create a new viewcontroller is alloc my custom view in loadView or viewDidLoad, e.g:
-(void)viewDidLoad
{
[super viewDidLoad];
detailView = [[DetailView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
detailView.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth;
self.view = detailView;
}
Normally this width & height is not correct for an iPhone5-screen (the actual view-frame is not set until viewWillAppear) but because of the autoresizingmask it all works out.
Then in the initWithFrame of the custom UIView DetailView, I alloc all subviews with CGRectZero, e.g:
-(id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
label = [[UILabel alloc] initWithFrame:CGRectZero];
[self addSubview:label];
}
}
Then I override layoutsubviews to set all frames of all subviews. This works perfectly for any screen size and any orientation, e.g:
-(void)layoutSubviews
{
[super layoutSubviews];
label.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
}
However, I just found out that layoutSubviews is not so great when you use animations, because when you use animations in an animationblock, layoutsubviews is called in the middle of the animation and it completely breaks the animation, e.g:
-(void)animateLabel
{
[UIView animateWithDuration:0.4f animations:^
{
label.transform = CGAffineTransformMakeTranslation(100, 100);
}];
}
I believe there are ugly workarounds this by using flags for each animation and in layoutsubviews use those flags to set the correct start or endframe of the animated block but I don't think I should have to create a flag for each animation I want to do.
So my problem is now: how am I supposed to have a custom UIView WITH animations that also automatically adjusts itself to rotations?
The only solution I can come up with right now (that I don't like):
Don't use layoutSubviews but use the setFrame/setBounds method of the custom UIView to set the frames of all subviews. Then check in the viewController every time a rotation occurs and then use the setFrame/setBounds method of the custom UIView to change all frames of all subviews. I don't like this solution because the rotation methods are different in iOS5 and iOS6 and I don't want to have to do this in every UIViewController with it's own custom UIView.
Any suggestions?
I have recently started overriding viewDidLayoutSubviews (many times instead of viewWillAppear) in my UIViewControllers.
Yes viewDidLayoutSubviews is called on rotations. (from comment)
The method fires after all the internal layouts have already been completed so all finalized frames should be setup, but still give you the time you need to make adjustments before the the view is visible and shouldn't have any issues with animations because you are not already inside an animation block.
viewcontroller.m
- (void)viewDidLayoutSubViews {
// At this point I know if an animation is appropriate or not.
if (self.shouldRunAnimation)
[self.fuView runPrettyAnimations];
}
fuView.m
- (void)runPrettyAnimations {
// My animation blocks for whatever layout I'd like.
}
- (void)layoutSubviews {
// My animations are not here, but non animated layout changes are.
// - Here we have no idea if our view is visible to the user or may appear/disappear
// partway through an animation.
// - This also might get called far more than we intend since it gets called on
// any frame updates.
}
The UITableView I am using has a custom UItableViewCell. This custom cell has a subview (an UIView subclass) to it. I use the drawRect of the custom UIView subclass to place all the text to be displayed by the cell.
And in the drawRect (of UIView subclass) I do the following
/*
// This piece of code is called when setNeedsDisplay is called
*/
- (void)drawRect:(CGRect)rect
{
self.layer.cornerRadius = 10.0f;
self.layer.backgroundColor = [[UIColor orangeColor] CGColor];
self.layer.borderColor = [[UIColor lightGrayColor] CGColor];
self.layer.borderWidth = 3.0f;
}
However my custom cell is a black square like this
But I do see the intended behavior if I select the row. Like shown below
Whats going on ?
Your drawRect: method does not draw anything; the code that you put in there belongs in your initWithFrame: implementation.
You should manipulate the layer configuration in the initializer; in your drawRect: you should call functions of your CGContextRef based on the state of the view. For example, to draw some text you would use CGContextShowTextAtPoint, to draw some lines you would use CGContextAddLineToPoint, and so on.
See this question for information on the relationship between drawRect: and the CALayer of your UIView.
Try to set self.layer.masksToBounds = YES and (maybe) self.opaque = NO during your UIView's (the one where drawRect is overridden) initialization. (see this question)
Try to disable the selection highlight of the cell by using
[cell setSelectionStyle:UITableViewCellSelectionStyleNone];
You are doing one mistake:
Please go to the view in side the UItableViewCell and check the background color it may be black or something others, Reset it to clear color then check your result,
I want to have my buttons in my iOS app to have a red gradient. At first I was using images to do this, but then realised I can do it with QuartzCore framework. I have the following implementation file:
#import "RedButton.h"
#implementation RedButton
#synthesize gradientLayer = _gradientLAyer;
- (void)awakeFromNib;
{
// Initialize the gradient layer
self.gradientLayer = [[CAGradientLayer alloc] init];
// Set its bounds to be the same of its parent
[self.gradientLayer setBounds:[self bounds]];
// Center the layer inside the parent layer
[self.gradientLayer setPosition:
CGPointMake([self bounds].size.width/2,
[self bounds].size.height/2)];
// Insert the layer at position zero to make sure the
// text of the button is not obscured
[[self layer] insertSublayer:self.gradientLayer atIndex:0];
// Set the layer's corner radius
[[self layer] setCornerRadius:5.0f];
// Turn on masking
[[self layer] setMasksToBounds:YES];
// Display a border around the button
// with a 1.0 pixel width
[[self layer] setBorderColor:[UIColor colorWithRed:(158.0f/255.0f) green:0.0f blue:0.0f alpha:1.0f].CGColor];
[[self layer] setBorderWidth:1.0f];
[self.gradientLayer setColors:[NSArray arrayWithObjects:
(id)[[UIColor colorWithRed:(214.0f/255.0f) green:0.0f blue:0.0f alpha:1.0f] CGColor],
(id)[[UIColor colorWithRed:(141.0f/255.0f) green:0.0f blue:0.0f alpha:1.0f] CGColor], nil]];
[[self layer] setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect;
{
[super drawRect:rect];
}
- (void)dealloc {
// Release our gradient layer
self.gradientLayer = nil;
[super dealloc];
}
#end
First question - am I right to use awakeFromNib here? Or should I be using initWithFrame?
Second question - originally I was using images and using interface builder to set the default and highlighted states of the button. Now that I'm not using images, how can I set the appearance of the button to change when it's highlighted? I just want to reverse the gradient.
Third question - I've seen it written in some places that you shouldn't subclass UIButton. If not, how would I change all my buttons to have this gradient without duplicating a lot of code?
Thanks in advance.
edit1: Misread the part about images.
You should be able to do something like this to set the button states natively as image using your gradients
// your code for setting up the gradient layer comes first
UIGraphicsBeginImageContext(CGSizeMake(1, [self bounds].size.height));
[gradientLayer renderInContext: UIGraphicsGetCurrentContext()];
UIImage *bgImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[self setBackGroundImage:bgImage forState:UIControlStateWhatever] // replace with correct states
============================
I recommend putting your initialization code in a function other than awakeFromNib (for the case where the button is actually NOT being used in a nib but possibly being created in code). You should create a custom initialization function and call it in both initWithCoder and initWithFrame This answer shows a pretty good pattern for doing so.
You can set the background for different states in your initialization by calling
[self setBackGroundImage: forState];
Where in this case your state would be UIControlStateHighlighted.
That aside, one argument against subclassing in this situation is that you're not actually defining any custom behavior, you're just trying to reuse some styling code. A subclass isn't necessary here and you can do something as simple as creating a formatting function somewhere (in your viewcontroller maybe, or some function in another class) that takes a UIButton as a scenario and performs all of that initialization code on it. This way, you will not have your buttons locked up in a subclass (which is useful if you actually end up using another UIButton subclass.. I for example like to use one that defines custom touch behavior allowing the button to be a non-rectangular shape (and have its touch area limited as such).
Another argument I've seen is that UIButton contains some factory functions that may return a button of a different type than your subclass, but you may never run into this issue if you don't use those functions.
I am wondering how one might animate a CALayer's bounds so, on each bounds change, the layer calls drawInContext:. I've tried the 2 following methods on my CALayer subclass:
Setting needsDisplayOnBoundsChange to YES
Returning YES for the + (BOOL)needsDisplayForKey:(NSString*)key for the bounds key
Neither work. CALayer seems determined to use the layer's original contents and simply scale them according to contentsGravity (which, I assume, is for performance.) Is their a workaround for this or am I missing something obvious?
EDIT: And, incidentally, I noticed that my custom CALayer subclass is not calling initWithLayer: to create a presentationLayer - weird.
Thanks in advance,
Sam
You can use the technique outlined here: override CALayer's +needsDisplayForKey: method and it will redraw its content at every step of the animation.
I'm not sure that setting the viewFlags would be effective. The second solution definitely won't work:
The default implementation returns NO. Subclasses should * call super
for properties defined by the superclass. (For example, * do not try
to return YES for properties implemented by CALayer, * doing will
have undefined results.)
You need to set the view's content mode to UIViewContentModeRedraw:
UIViewContentModeRedraw, //redraw on bounds change (calls -setNeedsDisplay)
Check out Apple's documentation on providing content with CALayer's. They recommend using the CALayer's delegate property instead of subclass, which might be a lot easier than what you're trying now.
I don't know if this entirely qualifies as a solution to your question, but was able to get this to work.
I first pre-drew my contents image into a CGImageRef.
I then overrode the -display method of my layer INSTEAD OF -drawInContext:. In it I set contents to the pre-rendered CGImage, and it worked.
Finally, your layer also needs to change the default contentsGravity to something like #"left" to avoid the contents image being drawn scaled.
The problem I was having was that the context getting passed to -drawInContext: was of the starting size of the layer, not the final post-animation size. (You can check this with the CGBitmapContextGetWidth and CGBitmapContextGetHeight methods.)
My methods are still only called once for the entire animation, but setting the layer's contents directly with the -display method allows you to pass an image larger than the visible bounds. The drawInContext: method does not allow this, as you cannot draw outside the bounds of the CGContext context.
For more about the difference between the different layer drawing methods, see http://www.apeth.com/iOSBook/ch16.html
I'm running into the same problem recently. This is what I found out:
There is a trick you can do it, that is animating a shadow copy of bounds like:
var shadowBounds: CGRect {
get { return bounds }
set { bounds = newValue}
}
then override CALayer's +needsDisplayForKey:.
However, this MAY NOT be what you want to do if your drawing depends on bounds. As you have already noticed, core animation simply scales the contents of layer to animate bounds. This is true even if you do the above trick, that is, the contents are scaled even if the bounds changed during animation. The result is your animation of drawings looks inconsistent.
How to resolve it? Since the content is scaled, you can calculate the values of custom variables determining your drawing by reverse-scaling them so that your drawing on the final but scaled content looks the same as the original and unscaled one, then set the fromValues to these values, the toValues to their old values, animate them at the same time with bounds. If the final values are to be changed, set the toValues to these final values. You must animate at least one custom variable so as to causing the redraw.
This is your custom class:
#implementation MyLayer
-(id)init
{
self = [super init];
if (self != nil)
self.actions = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNull null], #"bounds",
nil];
return self;
}
-(void)drawInContext:(CGContextRef)context
{
CGContextSetRGBFillColor(context,
drand48(),
drand48(),
drand48(),
1);
CGContextFillRect(context,
CGContextGetClipBoundingBox(context));
}
+(BOOL)needsDisplayForKey:(NSString*)key
{
if ([key isEqualToString:#"bounds"])
return YES;
return [super needsDisplayForKey:key];
}
#end
These are additions to xcode 4.2 default template:
-(BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
// create and add layer
MyLayer *layer = [MyLayer layer];
[self.window.layer addSublayer:layer];
[self performSelector:#selector(changeBounds:)
withObject:layer];
return YES;
}
-(void)changeBounds:(MyLayer*)layer
{
// change bounds
layer.bounds = CGRectMake(0, 0,
drand48() * CGRectGetWidth(self.window.bounds),
drand48() * CGRectGetHeight(self.window.bounds));
// call "when idle"
[self performSelector:#selector(changeBounds:)
withObject:layer
afterDelay:0];
}
----------------- edited:
Ok... this is not what you asked for :) Sorry :|
----------------- edited(2):
And why would you need something like that? (void)display may be used, but documentation says it is there for setting self.contents...
Could someone please explain to me how to draw a
string using UIStringDrawing instead of using a label? here is my code but for some reason it compiles and the screen is blank...
//
// MainViewController.m
// DotH
//
#define WINDOW_FRAME [[UIScreen mainScreen] bounds]
#define SCREEN_FRAME [UIScreen mainScreen].applicationFrame
#define GRAY [UIColor grayColor]
#define BLACK [UIColor blackColor]
#implementation MainViewController
- (void)loadView {
UIView *view = [[UIView alloc] initWithFrame:SCREEN_FRAME];
[view setBackgroundColor:GRAY];
[BLACK setFill];
NSString *string = #"Hey Dude!";
[string drawAtPoint:CGPointMake(50, 50) withFont:[UIFont systemFontOfSize:14]];
self.view = view;
[view release];
}
#end
The line
[string drawAtPoint:CGPointMake(50, 50) withFont:[UIFont systemFontOfSize:14]];
needs to be in the view's drawRect: method.
This is because just before drawRect: is called, the rectangle passed as an argument to drawRect: is erased. So if you try to do custom drawing anywhere other than a view's drawRect: method, the stuff you draw will get erased whenever drawRect: is called. (Not to mention that calling drawAtPoint: is meaningless if not done by code within a UIView.)
You will need to make a custom subclass of UIView, and that subclass will need a custom drawRect: method. If you still want the view controller to be the entity responsible for deciding what string should be drawn and how, you should give your UIView subclass a method like
- (void)addString:(NSString *)string atPoint:(CGPoint)point withFont:(UIFont *)font;
That method can store that information, in, e.g., three NSMutableArrays (one of strings, one of points, and one of fonts), and increment a counter of how many strings have been added. Then, your view's drawRect: method can draw the strings described in those arrays. To add a string, your view controller just calls addString:atPoint:withFont: on your view.