My goal is to create a custom UIButton subclass, that creates a round (or round rect) button with some additional features.
After some searching, I found that simply setting the cornerRadius of the button layer is the easiest way to make the button round:
#implementation MyRoundButton
-(id)initWithFrame:(CGRect)frame{
if (self = [super initWithFrame:frame])
[self setupView];
return self;
}
-(id)initWithCoder:(NSCoder *)aDecoder{
if(self = [super initWithCoder:aDecoder])
[self setupView];
return self;
}
-(void)setupView {
self.layer.cornerRadius = MIN(self.frame.size.height, self.frame.size.width) / 2.0;
}
This works fine to, as long as I do not override drawRect:
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
}
Of course I would like to add some custom drawing code to drawRect: but even if only [super drawRect:rect] the button is not round anymore: It drawn as rectangle within its bounds.
How is this possible? How can overriding a method with a simple super call change the behavior at all?
How can I avoid this problem? I already tried to let the layer unchanged and to simply draw the background manually in drawRect:. However the button draws its background rect anyway and my custom drawing is on top of it.
So, how to solve this?
The layer in your example conceptually contains a background with a corner radius, but the empty override of drawRect: causes that to be ignored. That's because, ordinarily, an instance of UIView depends on its built-in layer (an instance of CALayer) to render its content, so drawRect: isn't called. However, the layer is capable of delegating drawing behavior to its view, and will do so if you implement drawRect:.
If you're simply subclassing UIView, this shouldn't present any problem. However, subclassing a framework component such as UIButton is a bit dicier. One potential issue is the call to -[super drawRect:]; it's hard to know precisely what mischief that might cause, but that may be the source of the problem. By default, a UIButton doesn't need to do any custom drawing; it contains a nested instance of a private class, UIButtonLabel, that draws the button's title.
Instead of trying to override the drawing behavior of a class whose inner details are private, consider using one or more static images and simply setting the button's background image property for one or more states.
See if this changes anything...
-(void)setupView {
self.layer.cornerRadius = MIN(self.frame.size.height, self.frame.size.width) / 2.0;
self.clipsToBounds = YES;
}
Related
I want to create a UIView subclass that masks itself so that any child view's I add to it are cropped by a circle. I want this view and it's child views to be defined in IB, so that I can easily define layout constraints to the children. So far I have the following...
#interface BubbleView ()
// eg: this is an example of a child view that would be "under" a mask
#property(weak,nonatomic) IBOutlet UIImageView *imageView;
#end
#implementation BubbleView
// not really sure if this kind of init is the right pattern, but it seems to work and
// I don't think this is my current problem??
+ (instancetype)bubbleViewFromNib {
BubbleView *view = [[NSBundle mainBundle] loadNibNamed:#"BubbleView" owner:nil options:nil][0];
UIImage *_maskingImage = [UIImage imageNamed:#"circlemask"];
CALayer *maskingLayer = [CALayer layer];
[view.layer addSublayer:maskingLayer];
maskingLayer.contents = (__bridge id _Nullable)(_maskingImage.CGImage);
view.layer.mask = maskingLayer;
return view;
}
- (void)layoutSubviews {
self.layer.mask.frame = self.bounds;
}
(note: I give the view a purple color in IB so I can see what's going on)
This almost works, but when the owning view controller resizes this view, like this...
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
BubbleView *b = (BubbleView *)[self.view viewWithTag:222];
//b.transform = CGAffineTransformScale(b.transform, 1.2, 1.2);
b.frame = CGRectInset(b.frame, -30,-30);
}
The mask does something weird: It stays small "jumps" to the upper left corner of the view, then very quickly animates to the correct size (bigger by 30,30). Why would it animate?
Before...
Fast animation like this...
Placing NSLog in layoutSubviews, I notice it gets called twice, which is strange, but still not enough times to explain the quick animation.
Also, when I change the transform instead of the frame, it resizes perfectly, with no animation. But I need to do both frame and transform changes.
Can someone tell me where I've gone wrong?
When setting an animatable property of a layer, unless that layer is a UIView's primary layer, implicit animation is the default. Moreover, the frame property is merely a facade for the bounds and position properties. For this reason, you should never set a layer's frame directly; always set the bounds and position yourself. If you don't want animation, you'll need to turn off implicit animation for these properties; the simplest way is to turn it off entirely for the current CATransaction (setDisableActions to true), but there are more subtle and precise ways to accomplish the same thing.
I'm overriding drawRect: in one of my views and it works even without calling [super drawrect:rect]. How does that work?
- (void)drawRect:(CGRect)rect{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetBlendMode(context, kCGBlendModeDestinationOver);
[[UIColor clearColor] setFill];
UIRectFill(CGRectMake(0,0,100,100));
}
From the documentation:
"The default implementation of this method does nothing.Subclasses
that use technologies such as Core Graphics and UIKit to draw their
view’s content should override this method and implement their drawing
code there"
Also from the documentation..
"If you subclass UIView directly, your implementation of this method
does not need to call super. However, if you are subclassing a
different view class, you should call super at some point in your
implementation."
Link to the documentation
I am assuming, your super class is UIView. Hence it works even if [super drawrect:rect] is not called. Because the drawRect: because UIView does nothing. So it makes no difference in this case whether or not you call [super drawrect:rect].
Basically, it works because the machinery that sets up the graphics context for drawing, etc, doesn't live in the UIView implementation of -drawRect: (which is empty).
That machinery does live somewhere, of course, let's pretend in every UIView there's some internal method like this-- names of methods, functions, and properties are invented to protect the innocent:
- (void)_drawRectInternal:(CGRect)rect
{
// Setup for drawing this view.
_UIGraphicsContextPushTransform(self._computedTransform);
// Call this object's implementation of draw (may be empty, or overridden).
[self drawRect:rect];
// draw our subviews:
for (UIView * subview in [self subviews]) {
[subview _drawRectInternal:rect];
}
// Teardown for this view.
_UIGraphicsContextPopTransform();
}
This is a cleaner way of building the frameworks than relying on a subclasser to do the right thing in a common override, where getting it wrong would have result in very puzzling bad behavior. (And this wouldn't really be possible at all since the draw might need a setup and a teardown step.)
I'm sure subview doesn't work without calling [super drawrect:rect].If you need to keep the superview's drawing ,you must add [super drawrect:rect] in the subview's drawrect:rect,or you'll replace the drawing.
Thanks.. There is no need to call super, because the superclass here is UIView, whose drawRect: does nothing.
In general, we call setNeedDisplay if we want recall drawRect for drawing.
I'm new to iOS UIView drawing and I'm really trying to implement a custom UIView class in a standard way.
The UIView class that I'm working on now is simple: it has mostly static background shapes that form a composite shape. I also want to add animation to this UIView class so that a simple shape can animate its path on top of the static shapes.
How I'm currently implementing this
Right now, I'm implementing the static drawing in the drawRect: method of the UIView subclass. This works fine. I also have not implemented animation of the single shape yet.
What I'm looking to have answered
Question 1:
Is it better to have the static shapes drawn in the drawRect: method as I'm currently doing it, or is there a benefit to refactoring all of the shapes being drawn into class-extension-scoped CAShapeLayer properties and doing something like:
-(instancetype) initWithFrame:(CGRect) frame
{
if (self = [super initWithFrame:frame])
{
[self setupView];
}
return self;
}
-(void) setupView // Allocate, init, and setup one-time layer properties like fill colorse.
{
self.shapeLayer1 = [CAShapeLayer layer];
[self.layer addSubLayer:shapeLayer1];
self.shapeLayer2 = [CAShapeLayer layer];
[self.layer addSubLayer:shapeLayer2];
// ... and so on
}
-(void) layoutSubviews // Set frames and paths of shape layers here.
{
self.shapeLayer1.frame = self.bounds;
self.shapeLayer2.frame = self.bounds;
self.shapeLayer1.path = [UIBezierPath somePath].CGPath;
self.shapeLayer2.path = [UIBezierPath somePath].CGPath;
}
Question 2:
Regardless of whether I implement the static shapes as CAShapeLayers or in drawRect:, is the best way to implement an animatable shape for this UIView a CAShapeLayer property that is implemented as the other CAShapeLayers would be implemented above? Creating a whole separate UIView class just for one animated shape and adding it as a subview to this view seems silly, so I'm thinking that a CAShapeLayer is the way to go to accomplish that.
I would appreciate any help with this very much!
You could do it either way, but using shape layers for everything would probably be cleaner and faster. Shape layers have built-in animation support using Core Animation.
It's typically faster to avoid implementing drawRect and instead let the system draw for you.
Edit:
I would actually word this more strongly than I did in 2014. The underlying drawing engine in iOS is optimized for tiled rendering using layers. You will get better performance and smoother animation by using CALayers and CAAnimation (or higher level UIView or SwiftUI animation which is built on CAAnimation.)
I have a UIView on which I draw a UIBezierPath by finger.
When I rezoom the view (say after a path is drawn) a redraw function is triggered, which rescales the BezierPath:
- (void)redrawPathsWithScale:(float)scale
{
[_path applyTransform:CGAffineTransformMakeScale(scale, scale)];
[self setNeedsDisplay];
}
setNeedsDisplay causes drawRect to get called.
Now every time I zoom in to a absolute scale somewhere near x6 I immediately get a memory warning, and the App crashes.
My drawRect method looks like this:
- (void)drawRect:(CGRect)rect
{
[_path strokeWithBlendMode:kCGBlendModeNormal alpha:1.0];
}
The Curious thing is: Not implementing drawRect at all removes the memory warning. Implementing an empty drawRect still causes a crash!
Does adding [super drawRect:rect]; make any difference?
The Apple Documentation for drawRect states:
If you subclass UIView directly, your implementation of this method does not need to call super. However, if you are subclassing a different view class, you should call super at some point in your implementation.
If you're subclassing UIView, you should be fine but it might be worth checking this just in case.
I don't really understand how CALayer's display and drawInContext relate to drawRect in the view.
If I have an NSTimer that sets the [self.view setNeedsDisplay] every 1 second, then drawRect is called every 1 second, as shown by an NSLog statement inside of drawRect.
But if I subclass a CALayer and use that for the view, if I make the display method empty, then now drawRect is never called. Update: But display is called every 1 second, as shown by an NSLog statement.
If I remove that empty display method and add an empty drawInContext method, again, drawRect is never called. Update: But drawInContext is called every 1 second, as shown by an NSLog statement.
What is exactly happening? It seems that display can selectively call drawInContext and drawInContext can selectively somehow call drawRect (how?), but what is the real situation here?
Update: there is more clue to the answer:
I changed the CoolLayer.m code to the following:
-(void) display {
NSLog(#"In CoolLayer's display method");
[super display];
}
-(void) drawInContext:(CGContextRef)ctx {
NSLog(#"In CoolLayer's drawInContext method");
[super drawInContext:ctx];
}
So, let's say, if there is a moon (as a circle drawn by Core Graphics) at location (100,100) in the View, and now I change it to location (200,200), naturally, I will call [self.view setNeedsDisplay], and now, CALayer will have no cache at all for the new view image, as my drawRect dictates how the moon should now be displayed.
Even so, the entry point is CALayer's display, and then CALayer's drawInContext: If I set a break point at drawRect, the call stack shows:
So we can see that CoolLayer's display is entered first, and it goes to CALayer's display, and then CoolLayer's drawInContext, and then CALayer's drawInContext, even though in this situation, no such cache exist for the new image.
Then finally, CALayer's drawInContext calls the delegate's drawLayer:InContext. The delegate is the view (FooView or UIView)... and drawLayer:InContext is the default implementation in UIView (as I did not override it). It is finally that drawLayer:InContext calls drawRect.
So I am guessing two points: why does it enter CALayer even though there is no cache for the image? Because through this mechanism, the image is drawn in the context, and finally returns to display, and the CGImage is created from this context, and then it is now set as the new image cached. This is how CALayer caches images.
Another thing I am not quite sure is: if [self.view setNeedsDisplay] always trigger drawRect to be called, then when can a cached image in CALayer be used? Could it be... on Mac OS X, when another window covers up a window, and now the top window is moved away. Now we don't need to call drawRect to redraw everything, but can use the cached image in the CALayer. Or on iOS, if we stop the app, do something else, and come back to the app, then the cached image can be used, instead of calling drawRect. But how to distinguish these two types of "dirty"? One is a "unknown dirty" -- that the moon needs to be redrawn as dictated by the drawRect logic (it can use a random number there for the coordinate too). The other types of dirty is that it was covered up or made to disappear, and now needs to be re-shown.
When a layer needs to be displayed and has no valid backing store (perhaps because the layer received a setNeedsDisplay message), the system sends the display message to the layer.
The -[CALayer display] method looks roughly like this:
- (void)display {
if ([self.delegate respondsToSelector:#selector(displayLayer:)]) {
[[self.delegate retain] displayLayer:self];
[self.delegate release];
return;
}
CABackingStoreRef backing = _backingStore;
if (!backing) {
backing = _backingStore = ... code here to create and configure
the CABackingStore properly, given the layer size, isOpaque,
contentScale, etc.
}
CGContextRef gc = ... code here to create a CGContext that draws into backing,
with the proper clip region
... also code to set up a bitmap in memory shared with the WindowServer process
[self drawInContext:gc];
self.contents = backing;
}
So, if you override display, none of that happens unless you call [super display]. And if you implement displayLayer: in FooView, you have to create your own CGImage somehow and store it in the layer's contents property.
The -[CALayer drawInContext:] method looks roughly like this:
- (void)drawInContext:(CGContextRef)gc {
if ([self.delegate respondsToSelector:#selector(drawLayer:inContext:)]) {
[[self.delegate retain] drawLayer:self inContext:gc];
[self.delegate release];
return;
} else {
CAAction *action = [self actionForKey:#"onDraw"];
if (action) {
NSDictionary *args = [NSDictionary dictionaryWithObject:gc forKey:#"context"];
[action runActionForKey:#"onDraw" object:self arguments:args];
}
}
}
The onDraw action is not documented as far as I know.
The -[UIView drawLayer:inContext:] method looks roughly like this:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)gc {
set gc's stroke and fill color spaces to device RGB;
UIGraphicsPushContext(gc);
fill gc with the view's background color;
if ([self respondsToSelector:#selector(drawRect:)]) {
[self drawRect:CGContextGetClipBoundingBox(gc)];
}
UIGraphicsPopContext(gc);
}
The update procedure of UIView is based on the dirty state meaning the view is not likely to be redrawn if there's no change at it's appearance.
That is internal implementation mentioned at the developer reference.
Implementing a drawInContext or display or drawRect tells the OS which one you want called when the view is dirty (needsDisplay). Pick the one you want called for a dirty view and implement that, and don't put any code you depend on getting executed in the others.