In a magazin I read about some nice extensions for the UIView class. You will be able to add a border with corner radian or a drop shadow to any UIView.
#implementation UIView (Extentions)
-(void) enableRoundRectsWithValue:(float)value
{
self.layer.masksToBounds = true;
self.layer.cornerRadius = value;
}
-(void) enableShadow
{
self.layer.masksToBounds = false;
self.layer.shadowOffset = CGSizeMake(0,2);
self.layer.shadowOpacity = 0.5;
}
#end
While these methods work fine for themselves they don't play nice together. I can't have a corner radius and a shadow. At least not like you expect them to be. I guess because masksToBounds is set to true in one method and false in the other.
How can I get a UIView with a corner radius and also a shadow (with the same corner radius)?
It's kind of old but more people with the same problem can get here looking for a solution.
I guess this post can help. It explains a little about CALayer and about mixing effects, including corner radius and shadows.
Related
I am not good with CALayer but I need to draw a plus (+) sign and I don't want to use an image as I want to animate the drawing. Any help?
After all the down votes, I was able to do it myself. Here's how for others who might need this
CGFloat height = 2.f;
CGFloat width = 3.f;
CGFloat cornerRadius = 1.f;
CALayer *hLayer = [CALayer layer];//this is the left - right stroke
hLayer.frame = CGRectMake(0, CGRectGetMidY(self.bounds)-(height/2), width, height);
hLayer.cornerRadius = cornerRadius;
CALayer *vLayer = [CALayer layer];// this is the top - bottom stroke
vLayer.frame = CGRectMake(CGRectGetMidX(self.bounds) - (height/2), -3,height, width);
vLayer.cornerRadius = cornerRadius;
[self.layer addSublayer:hLayer];
[self.layer addSublayer:vLayer];
As luk2302 says, use a CAShapeLayer.
You can install a CGPath into a CAShapeLayer. You can get a CGPath from any UIBezierPath. (It has a CGPath property that lets you get to the underlying CGPath object for any UIBezierPath.)
I suggest reading up on UIBezierPath. It has methods moveToPoint and addLineToPoint.
You'd move to the top of your plus, add a line down, then move to the left of your plus and add a line across.
Note that you can also animate images, depending on the type of animation you are after. What kind of animation do you need to do?
Another simple way would be to use two UIView with the same background color and add them as a subview to another UIView. Then you can use the UIView animateWithDuration:... method to do your simple animations.
I have a class that I've used for a long time that draws a border around a UIView (or anything that inherits from UIView) and give that view rounded corners. I was doing some testing today (after upgrading to Xcode 7 and compiling on iOS 8.3 for the first time) and noticed that the right edge of the UIView is being truncated when I run on iPhone 6/6+ on the simulator (I don't have the actual devices, but I assume the results would be the same).
Here is a simple example. Notice how I've given the superview a red background to make this jump out. The subview is a UIView that has a fixed height and is vertically aligned to the center of the view. That works. The leading and trailing edges are supposed to be pinned to the edge of the superview, as you can see in the constraints in this image. Notice how the inner UILabel and UIButton are centered as they should be, but the UIView container is getting truncated on the right, even though the border is being drawn.
Here are the storyboard settings. The UIView that has the borders is of a fixed height, centered vertically, with leading and trailing edges pinned to the superview:
And finally, here is the code. In the UIViewController, I ask for borders like this. If I comment this code out, the view looks just fine, other than I don't have the borders that I want, of course.
BorderMaker *borderMaker = [[BorderMaker alloc] init];
[borderMaker makeBorderWithFourRoundCorners:_doneUpdatingView borderColor:[SharedVisualElements primaryFontColor] radius:8.0f];
And the BorderMaker class:
#implementation BorderMaker
- (void) makeBorderWithFourRoundCorners : (UIView *) view
borderColor : (UIColor *) borderColor
radius : (CGFloat) radius
{
UIRectCorner corners = UIRectCornerAllCorners;
CGSize radii = CGSizeMake(radius, radius);
[self drawBorder : corners
borderColor : borderColor
view : view
radii : radii];
}
- (void) drawBorder : (UIRectCorner) corners
borderColor : (UIColor *) borderColor
view : (UIView *) view
radii : (CGSize) radii
{
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:view.bounds
byRoundingCorners:corners
cornerRadii:radii];
// Mask the container view’s layer to round the corners.
CAShapeLayer *cornerMaskLayer = [CAShapeLayer layer];
[cornerMaskLayer setPath:path.CGPath];
view.layer.mask = cornerMaskLayer;
// Make a transparent, stroked layer which will dispay the stroke.
CAShapeLayer *strokeLayer = [CAShapeLayer layer];
strokeLayer.path = path.CGPath;
strokeLayer.fillColor = [UIColor clearColor].CGColor;
strokeLayer.strokeColor = borderColor.CGColor;
strokeLayer.lineWidth = 1.5; // the stroke splits the width evenly inside and outside,
// but the outside part will be clipped by the containerView’s mask.
// Transparent view that will contain the stroke layer
UIView *strokeView = [[UIView alloc] initWithFrame:view.bounds];
strokeView.userInteractionEnabled = NO; // in case your container view contains controls
[strokeView.layer addSublayer:strokeLayer];
// configure and add any subviews to the container view
// stroke view goes in last, above all the subviews
[view addSubview:strokeView];
}
Somewhere in that class, it seems that the views bounds are not reflecting the fact that AutoLayout has stretched the view to fill the larger iPhone 6/6+ screen width. Just a guess since I am out of ideas. Any help is appreciated. Thanks!
BorderMaker creates various layers and views based on the current size of the input view. How do those layers and views get resized when the input view changes size? Answer: they don't.
You could add code to update the size in various ways, but I wouldn't recommend it. Since you're rounding all four corners anyway, you can solve this better by just using the existing CALayer support for drawing a border, rounding the corners, and masking its contents.
Here's a simple BorderView class:
BorderView.h
#import <UIKit/UIKit.h>
IB_DESIGNABLE
#interface BorderView : UIView
#property (nonatomic, strong) IBInspectable UIColor *borderColor;
#property (nonatomic) IBInspectable CGFloat borderWidth;
#property (nonatomic) IBInspectable CGFloat cornerRadius;
#end
BorderView.m
#import "BorderView.h"
#implementation BorderView
- (void)setBorderColor:(UIColor *)borderColor {
self.layer.borderColor = borderColor.CGColor;
}
- (UIColor *)borderColor {
CGColorRef cgColor = self.layer.borderColor;
return cgColor ? [UIColor colorWithCGColor:self.layer.borderColor] : nil;
}
- (void)setBorderWidth:(CGFloat)borderWidth {
self.layer.borderWidth = borderWidth;
}
- (CGFloat)borderWidth {
return self.layer.borderWidth;
}
- (void)setCornerRadius:(CGFloat)cornerRadius {
self.layer.cornerRadius = cornerRadius;
}
- (CGFloat)cornerRadius {
return self.layer.cornerRadius;
}
#end
Now, if you create a view in your storyboard and set its custom class to BorderView, you can set up its border right in the storyboard:
Note that I set “Clip Subviews” in the storyboard, so it'll clip subviews if they happen to go outside the rounded bounds of the BorderView.
If you set up constraints on the BorderView, they'll keep everything sized and positioned:
I solved this. The problem is that I was calling these BorderMaker methods from within the viewDidLoad method of the UIViewController. All I had to do was to move this to viewDidAppear. Presumably, as Rob Mayoff suggested, the autolayout wasn't finished by the time that the view was passed to the BorderMaker class, so it was getting a frame that hadn't considered the size of the screen, but rather was just using the width defined in the IB.
After some trial and error, it seems that viewDidAppear is the earliest life cycle method that I can use where autolayout is done with its work.
I'm trying to see if there's a way to create a UIView with a shadow behavior compliant with material design. My understanding is that the shadow gets more intense as the object is further removed from the background surface.
I can manually add a shadow like this, however this shadow does not calculate how intense the shadow should be (based on Z order).
button.layer.shadowPath = [UIBezierPath bezierPathWithRoundedRect:button.layer.bounds cornerRadius:11].CGPath;
button.layer.shadowOpacity = 1.0;
button.layer.shadowOffset = CGSizeMake(1,1);
button.layer.shadowColor = [UIColor blackColor].CGColor;
How do I create a UIView which will behave like Google's material design? I found this example, called Material Kit, however it simply hides shadow on touch
One way in which Google does this is by emphasizing the use of a
‘z-axis’ to create a sense of depth in which elements can occupy
different levels. In order to show that an element is occupying a
higher level on the z-axis, shadows are used around its border, being
cast on to the level below. What this creates is the illusion of an
interactive layer/element that exists above a different, lower
interactive layer/element.
You could subclass UIButton, and first set the shadow properties to your liking:
button.layer.shadowOffset = CGSizeMake(0, 1);
button.layer.shadowRadius = 5;
button.layer.shadowOpacity = 0;
Then change the shadowOpacity property when the highlighted state changes:
- (void)setHighlighted:(BOOL)highlighted
{
[super setHighlighted:highlighted];
self.layer.shadowOpacity = (highlighted ? 0.85 : 0);
}
In order to apply both a shadow and a rounded corner, you need to use 2 nested views. This is because the two effects require competing masksToBounds properties.
Nest 2 UIViews. Apply the shadow to the outer view, setting masksToBounds = false. Then apply the rounded corner to the inner view, which requires masksToBounds = true.
Note that, according to Apple docs, masksToBounds is equivalent to clipsToBounds.
I'm trying to get the circle below to have an opaque solid white color where the cornerRadius cuts out the UIView.
UIView *circle = [[UIView alloc] initWithFrame:CGRectMake(i * (todaySize + rightMargin), 0, smallSize, smallSize)];
circle.layer.cornerRadius = smallSize/2;
circle.layer.borderWidth = 0.5;
circle.layer.backgroundColor = [UIColor whiteColor].CGColor;
circle.backgroundColor = [UIColor whiteColor];
[self addSubview:circle];
I've tried a few things like setting the backgroundColor and opaque without any luck. Color Blended Layers still shows that the surrounding of the circle is transparent. Does anybody know how to solve this?
To avoid blending when using rounded corners, the rounding needs to be done in drawRect, rather than as a property on the layer. I needed UICollectionView cells with a rounded background in an app I'm working on. When I used layer.cornerRadius, the performance took a huge hit. Turning on color blended layers yielded the following:
Not what I was hoping for, I want those cells to be colored green indicating there is no blending occurring. To do this, I subclassed UIView into RoundedCornerView. My implementation is real short and sweet:
import UIKit
class RoundedCornerView: UIView {
static let cornerRadius = 40.0 as CGFloat
override func drawRect(rect: CGRect) {
let borderPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: RoundedCornerView.cornerRadius)
UIColor.whiteColor().set()
borderPath.fill()
}
}
Then I set the view I was rounding to be a RoundedCornerView in my nib. Running at that point yielded this:
Scrolling is buttery smooth and there is no longer any blending occurring. One odd side effect of this is that the view's backgroundColor property will color the excluded area of the corners, not the main body of the view. This means that the backgroundColor should be set to whatever is behind your view, not to the desired fill color.
Try using a mask to both avoid blending and dealing with the parent / child background color match.
override func layoutSubviews() {
super.layoutSubviews()
let maskLayer = CAShapeLayer()
maskLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 20).cgPath
layer.mask = maskLayer
}
Set clipsToBoundson the view or masksToBounds on the layer to YES
I was wondering what is the best way to draw a single point line?
My goal is to draw this line in a tableViewCell to make it look just like the native cell separator.
I don't want to use the native separator because i want to make in a different color and in a different position (not the bottom..).
At first i was using a 1px UIView and colored it in grey. But in Retina displays it looks like 2px.
Also tried using this method:
- (void)drawLine:(CGPoint)startPoint endPoint:(CGPoint)endPoint inColor:(UIColor *)color {
CGMutablePathRef straightLinePath = CGPathCreateMutable();
CGPathMoveToPoint(straightLinePath, NULL, startPoint.x, startPoint.y);
CGPathAddLineToPoint(straightLinePath, NULL, endPoint.x, endPoint.y);
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = straightLinePath;
UIColor *fillColor = color;
shapeLayer.fillColor = fillColor.CGColor;
UIColor *strokeColor = color;
shapeLayer.strokeColor = strokeColor.CGColor;
shapeLayer.lineWidth = 0.5f;
shapeLayer.fillRule = kCAFillRuleNonZero;
[self.layer addSublayer:shapeLayer];
}
It works in like 60% of the times for some reason.. Is something wrong with it?
Anyway ,i'd be happy to hear about a better way.
Thanks.
I did the same with a UIView category. Here are my methods :
#define SEPARATOR_HEIGHT 0.5
- (void)addSeparatorLinesWithColor:(UIColor *)color
{
[self addSeparatorLinesWithColor:color edgeInset:UIEdgeInsetsZero];
}
- (void)addSeparatorLinesWithColor:(UIColor *)color edgeInset:(UIEdgeInsets)edgeInset
{
UIView *topSeparatorView = [[UIView alloc] initWithFrame:CGRectMake(edgeInset.left, - SEPARATOR_HEIGHT, self.frame.size.width - edgeInset.left - edgeInset.right, SEPARATOR_HEIGHT)];
[topSeparatorView setBackgroundColor:color];
[self addSubview:topSeparatorView];
UIView *separatorView = [[UIView alloc] initWithFrame:CGRectMake(edgeInset.left, self.frame.size.height + SEPARATOR_HEIGHT, self.frame.size.width - edgeInset.left - edgeInset.right, SEPARATOR_HEIGHT)];
[separatorView setBackgroundColor:color];
[self addSubview:separatorView];
}
Just to add to Rémy's great answer, it's perhaps even simpler to do this. Make a class UILine.m
#interface UILine:UIView
#end
#implementation UILine
-(id)awakeFromNib
{
// careful, contentScaleFactor does NOT WORK in storyboard during initWithCoder.
// example, float sortaPixel = 1.0/self.contentScaleFactor ... does not work.
// instead, use mainScreen scale which works perfectly:
float sortaPixel = 1.0/[UIScreen mainScreen].scale;
UIView *topSeparatorView = [[UIView alloc] initWithFrame:
CGRectMake(0, 0, self.frame.size.width, sortaPixel)];
topSeparatorView.userInteractionEnabled = NO;
[topSeparatorView setBackgroundColor:self.backgroundColor];
[self addSubview:topSeparatorView];
self.backgroundColor = [UIColor clearColor];
self.userInteractionEnabled = NO;
}
#end
In IB, drop in a UIView, click identity inspector and rename the class to a UILine. Set the width you want in IB. Set the height to 1 or 2 pixels - simply so you can see it in IB. Set the background colour you want in IB. When you run the app it will become a 1-pixel line, that width, in that colour. (You probably should not be affected by any default autoresize settings in storyboard/xib, I couldn't make it break.) You're done.
Note: you may think "Why not just resize the UIView in code in awakeFromNib?" Resizing views upon loading, in a storyboard app, is problematic - see the many questions here about it!
Interesting gotchya: it's likely you'll just make the UIView, say, 10 or 20 pixels high on the storyboard, simply so you can see it. Of course it disappears in the app and you get the pretty one pixel line. But! be sure to remember self.userInteractionEnabled = NO, or it might get over your other, say, buttons!
2016 solution ! https://stackoverflow.com/a/34766567/294884
shapeLayer.lineWidth = 0.5f;
That's a common mistake and is the reason this is working only some of the time. Sometimes this will overlap pixels on the screen exactly and sometimes it won't. The way to draw a single-point line that always works is to draw a one-point-thick rectangle on integer boundaries, and fill it. That way, it will always match the pixels on the screen exactly.
To convert from points to pixels, if you want to do that, use the view's scale factor.
Thus, this will always be one pixel wide:
CGContextFillRect(con, CGRectMake(0,0,desiredLength,1.0/self.contentScaleFactor));
Here's a screen shot showing the line used as a separator, drawn at the top of each cell:
The table view itself has no separators (as is shown by the white space below the three existing cells). I may not be drawing the line in the position, length, and color that you want, but that's your concern, not mine.
AutoLayout method:
I use a plain old UIView and set its height constraint to 1 in Interface Builder. Attached it to the bottom via constraints. Interface builder doesn't allow you to set the height constraint to 0.5, but you can do it in code.
Make a connector for the height constraint, then call this:
// Note: This will be 0.5 on retina screens
self.dividerViewHeightConstraint.constant = 1.0/[UIScreen mainScreen].scale
Worked for me.
FWIW I don't think we need to support non-retina screens anymore. However, I am still using the main screen scale to future proof the app.
You have to take into account the scaling due to retina and that you are not referring to on screen pixels. See Core Graphics Points vs. Pixels.
Addition to Rémy Virin's answer, using Swift 3.0
Creating LineSeparator class:
import UIKit
class LineSeparator: UIView {
override func awakeFromNib() {
let sortaPixel: CGFloat = 1.0/UIScreen.main.scale
let topSeparatorView = UIView()
topSeparatorView.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: sortaPixel)
topSeparatorView.isUserInteractionEnabled = false
topSeparatorView.backgroundColor = self.backgroundColor
self.addSubview(topSeparatorView)
self.backgroundColor = UIColor.clear
self.isUserInteractionEnabled = false
}
}