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.
Related
I am trying to make a drawing application following this tutorial: http://www.effectiveui.com/blog/2011/12/02/how-to-build-a-simple-painting-app-for-ios/, and then I tried to make it such that I don't only draw on the entire screen, I only draw on a UIView that is inside of another UIView. (What I call a nested UIView)
My code is currently on github: https://github.com/sammy0025/SimplePaint
The only parts I tweaked with the original code from the tutorial is to change some class prefix names, enabled ARC so no deallocs, used storyboards (which works fine with the original code from the tutorial), and change this code in the main view controller implementation file (SPViewController.m):
-(void)viewDidLoad
{
[super viewDidLoad];
nestedView.backgroundColor=[UIColor colorWithWhite:0 alpha:0]; //This is to make the nested view transparent
SPView *paint = [[SPView alloc] initWithFrame:nestedView.bounds]; //original code is SPView *paint=[[SPView alloc] initWithFrame:self.view.bounds];
[nestedView addSubview:paint]; //original code is [self.view addSubview:paint];
}
My question is how do I make sure that I only draw inside the nested UIView?
Add clipping to your sub view. E.g. self.bounds would cover the whole area of a view.
UIBezierPath *p = [UIBezierPath bezierPathWithRect:self.bounds];
[p addClip];
I believe clipping for a view should be based on bounds initially and that you may shrink it by adding an new clipping. But there might be a difference between iOS and OS X here.
Rather than using an SPView inside a nested view, consider just changing the frame of the SPView to match what you want. This should solve your problem of only allowing drawing within a given rect.
I think you're seeing issues because of your storyboard configuration. I dont know much about storyboard, but I was able to get this to work programmatically by throwing out storyboard's view and building my own in viewDidAppear.
-(void)viewDidAppear:(BOOL)animated{
UIView *newView = [[UIView alloc] initWithFrame:self.view.bounds];
newView.backgroundColor = [UIColor greenColor];
SPView *newPaint = [[SPView alloc] initWithFrame:CGRectInset(self.view.bounds, 40,40)];
[newView addSubview:newPaint];
self.view = newView;
}
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.
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
When you try deleting a note in iPhone's Notes application, an UIActionSheet pops up. The sheet is translucent (but not black translucent). How is that achieved? Is it possible to make the background of UIActionSheet a certain color?
I usually implement the following delegate method:
- (void)willPresentActionSheet:(UIActionSheet *)actionSheet
Just to make a sample. In this case I use a stretched png as a background:
- (void)willPresentActionSheet:(UIActionSheet *)actionSheet {
UIImage *theImage = [UIImage imageNamed:#"detail_menu_bg.png"];
theImage = [theImage stretchableImageWithLeftCapWidth:32 topCapHeight:32];
CGSize theSize = actionSheet.frame.size;
// draw the background image and replace layer content
UIGraphicsBeginImageContext(theSize);
[theImage drawInRect:CGRectMake(0, 0, theSize.width, theSize.height)];
theImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[[actionSheet layer] setContents:(id)theImage.CGImage];
}
and this is the result:
alt text http://grab.by/4yF1
You can use the code below:
actionSheetObj.actionSheetStyle=UIActionSheetStyleBlackOpaque;
or
actionSheetObj.actionSheetStyle=UIActionSheetStyleBlackTranslucent;
actionSheetObj.actionSheetStyle=UIActionSheetStyleBlackTranslucent;
It's not too difficult. You can use the following code:
CGSize mySize = myActionSheet.bounds.size;
CGRect myRect = CGRectMake(0, 0, mySize.width, mySize.height);
UIImageView *redView = [[[UIImageView alloc] initWithFrame:myRect] autorelease];
[redView setBackgroundColor:[UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.5]];
[myActionSheet insertSubview:redView atIndex:0];
Just make sure you present the UIActionSheet before doing this or the size wont be set. That makes it kind of ugly, but you could do something like:
[myActionSheet showInView:self.view];
if (!redAdded) {
redAdded = YES;
//THE ABOVE CODE GOES HERE
}
You can definitely adjust the opacity by setting the alpha value. Interface Builder lets you edit the value directly, but in code I think you would do:
[[myActionSheet view] setOpaque:NO];
[[myActionSheet view] setAlpha:0.5];
I'm not sure if you need the setOpaque call or not - I think that is used to help optimize performance, as the iPhone won't try to render anything hidden by the opaque view.
It looks black to me (note: using 2.2.1). The only reason there's a color to it is because of the yellow behind it.
One option would be to use the black transparent background and find out the size and speed of the action sheet. Then create a view and animate it in at the same time you show the action sheet, just underneath it, to give it a tint different than the color naturally behind the action sheet. You would have to make the color view also translucent so you could see behind that as well.
I'm not sure if you can, but you might also try adjusting the opacity of the action sheet itself as well.
I am trying to set an image as the background of a custom UIButton. I was able to set a background image for the "rounded rect" UIButton in interface builder, but now the borders have vanished. Is there any way to retain the borders? Also, I would like to make the borders show up rectangular instead of rounded.
#import <QuartzCore/QuartzCore.h> // don't forget to add this in your file
CALayer * layer = [yourUIButton layer];
[layer setMasksToBounds:YES];
[layer setCornerRadius:0.0]; //when radius is 0, the border is a rectangle
[layer setBorderWidth:1.0];
[layer setBorderColor:[[UIColor grayColor] CGColor]];
i know this is an old question, but still i would like to post my answer here. since i was looking for answer but got this resolved myself in an easy way.
Check out the cornerRadius, borderWidth and borderColor properties of CALayer. These properties are new as of iPhone OS 3.0.
You probably want to subclass UIButton and then set these properties in the constructor. Your button has a layer property you can access to set these border properties.
Here's an example:
- (id)initWithFrame:(CGRect)frame {
if(self = [super initWithFrame:frame]) {
[self.layer setBorderWidth:1.0];
[self.layer setCornerRadius:5.0];
[self.layer setBorderColor:[[UIColor colorWithWhite:0.3 alpha:0.7] CGColor]];
}
return self;
}
You could also "User Defined Runtime Attributes" in IB to set rounded corners like done in image below.
http://i.stack.imgur.com/uBkuB.png
I'd just like to add to the answers provided by nanshi and Jonathan.
In order to access the border and cornerRadius properties of the layer you will first need to import the QuartzCore framework.
#import <QuartzCore/QuartzCore.h>
Otherwise you will not be able to access the properties.
When you set a background image in a button, the button outlines go away. Include them in your image, if you need different sizes look at the UIImage
stretchableImageWithLeftCapWidth:topCapHeight:
method.
You can set the border properties on the CALayer by accessing the layer property of the button.
First, add Quartz
#import <QuartzCore/QuartzCore.h>
Set properties:
[[myButton layer] setBorderWidth:2.0f];
[[myButton layer] setBorderColor:[UIColor greenColor].CGColor];