I have to create a custom UIView
that will have round corners, a border, a shadow and its drawRect()
method is overridden to provide custom drawing code with which several straight lines are drawn into the view (I need to use a fast, lightweight approach here since many of these views may be rendered).
The problem I'm currently facing is that the shadow doesn't apply anymore to the round corners as soon as I override drawRect()
in the view class (even without any custom code yet in it). See the attached image for the difference:
https://i.stack.imgur.com/R5HOn.jpg
In the view controller I'm using the following code:
view.layer.cornerRadius = 10;
view.layer.masksToBounds = true;
view.layer.borderColor = UIColor.grayColor().CGColor;
view.layer.borderWidth = 0.5;
view.layer.contentsScale = UIScreen.mainScreen().scale;
view.layer.shadowColor = UIColor.blackColor().CGColor;
view.layer.shadowOffset = CGSizeZero;
view.layer.shadowRadius = 5.0;
view.layer.shadowOpacity = 0.5;
view.layer.masksToBounds = false;
view.clipsToBounds = false;
In the overridden drawContext()
I would use something like:
var context:CGContext = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, UIColor.redColor().CGColor);
// Draw them with a 2.0 stroke width so they are a bit more visible.
CGContextSetLineWidth(context, 2.0);
CGContextMoveToPoint(context, 0.0, 0.0); //start at this point
CGContextAddLineToPoint(context, 20.0, 20.0); //draw to this point
CGContextStrokePath(context);
But as said above, the shadow problem occurs even without this code added.
Is there any other/better way to draw lightweight elements onto a view other than this approach that is compatible with round corners and shadows? I don't want to add any unnecessary extra views or image contexts to the view since these need to be light and performant.
This is a tricky one. UIView
's clipsToBounds
is necessary to get the rounded corners. But CALayer
's masksToBounds
has to be false
so the shadow is visible. Somehow, everything works if drawRect
is not overridden, but actually it shouldn't.
The solution is to create a superview to provide the shadow (in the demonstration below this is the shadowView
). You can test the following in Playground:
class MyView : UIView {
override func drawRect(rect: CGRect) {
let c = UIGraphicsGetCurrentContext()
CGContextAddRect(c, CGRectMake(10, 10, 80, 80))
CGContextSetStrokeColorWithColor(c , UIColor.redColor().CGColor)
CGContextStrokePath(c)
}
}
let superview = UIView(frame: CGRectMake(0, 0, 200, 200))
let shadowView = UIView(frame: CGRectMake(50, 50, 100, 100))
shadowView.layer.shadowColor = UIColor.blackColor().CGColor
shadowView.layer.shadowOffset = CGSizeZero
shadowView.layer.shadowOpacity = 0.5
shadowView.layer.shadowRadius = 5
let view = MyView(frame: shadowView.bounds)
view.backgroundColor = UIColor.whiteColor()
view.layer.cornerRadius = 10.0
view.layer.borderColor = UIColor.grayColor().CGColor
view.layer.borderWidth = 0.5
view.clipsToBounds = true
shadowView.addSubview(view)
superview.addSubview(shadowView)
Result:
https://i.stack.imgur.com/EZ6lt.png
I wrote a small extension to UIView to manage both rounded corners AND drop shadow. As the variables are @IBInspectable, everything can be set directly in the storyboard !
//
// UIView extensions.swift
//
// Created by Frédéric ADDA on 25/07/2016.
// Copyright © 2016 Frédéric ADDA. All rights reserved.
//
import UIKit
extension UIView {
@IBInspectable var shadow: Bool {
get {
return layer.shadowOpacity > 0.0
}
set {
if newValue == true {
self.addShadow()
}
}
}
@IBInspectable var cornerRadius: CGFloat {
get {
return self.layer.cornerRadius
}
set {
self.layer.cornerRadius = newValue
// Don't touch the masksToBound property if a shadow is needed in addition to the cornerRadius
if shadow == false {
self.layer.masksToBounds = true
}
}
}
func addShadow(shadowColor: CGColor = UIColor.black.cgColor,
shadowOffset: CGSize = CGSize(width: 1.0, height: 2.0),
shadowOpacity: Float = 0.4,
shadowRadius: CGFloat = 3.0) {
layer.shadowColor = shadowColor
layer.shadowOffset = shadowOffset
layer.shadowOpacity = shadowOpacity
layer.shadowRadius = shadowRadius
}
}
https://i.stack.imgur.com/2Leri.png
https://i.stack.imgur.com/symBo.png
There is one requirement : DON'T touch either clipToBounds on the view (in code or in IB) or masksToBound on the layer.
NB: one case in which it won't work : tableViews. As UITableView automatically triggers clipToBounds
under the hood, we can't have a drop shadow.
EDIT: as Claudia Fitero aptly noticed, you need to leave a small padding around the view to which you are adding a shadow, otherwise the shadow won't be visible. A 2px-padding is enough generally (depending on your shadow radius).
Shadow is dropped from whatever is inside view's layer. When you disable clipping, entire layer rectangle gets filled with default backgroundColor
so the shadow becomes rectangular too. Instead of clipping it with rounded mask just make layer's contents rounded, draw them yourself. And layer
's border is drawn around its bounds, so you need to draw it yourself too.
For example, in backgroundColor
setter set actual background color to clearColor
and use passed color in drawRect
to draw a rounded rect with.
In example below I declare properties as IBInspectable
and the whole class as IBDesignable
, so everything can be set in storyboard. This way you can even use default Background selector to change your rounded rect color.
Swift
@IBDesignable class RoundRectView: UIView {
@IBInspectable var cornerRadius: CGFloat = 0.0
@IBInspectable var borderColor: UIColor = UIColor.blackColor()
@IBInspectable var borderWidth: CGFloat = 0.5
private var customBackgroundColor = UIColor.whiteColor()
override var backgroundColor: UIColor?{
didSet {
customBackgroundColor = backgroundColor!
super.backgroundColor = UIColor.clearColor()
}
}
func setup() {
layer.shadowColor = UIColor.blackColor().CGColor;
layer.shadowOffset = CGSizeZero;
layer.shadowRadius = 5.0;
layer.shadowOpacity = 0.5;
super.backgroundColor = UIColor.clearColor()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setup()
}
override func drawRect(rect: CGRect) {
customBackgroundColor.setFill()
UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius ?? 0).fill()
let borderRect = CGRectInset(bounds, borderWidth/2, borderWidth/2)
let borderPath = UIBezierPath(roundedRect: borderRect, cornerRadius: cornerRadius - borderWidth/2)
borderColor.setStroke()
borderPath.lineWidth = borderWidth
borderPath.stroke()
// whatever else you need drawn
}
}
Swift 3
@IBDesignable class RoundedView: UIView {
@IBInspectable var cornerRadius: CGFloat = 0.0
@IBInspectable var borderColor: UIColor = UIColor.black
@IBInspectable var borderWidth: CGFloat = 0.5
private var customBackgroundColor = UIColor.white
override var backgroundColor: UIColor?{
didSet {
customBackgroundColor = backgroundColor!
super.backgroundColor = UIColor.clear
}
}
func setup() {
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize.zero
layer.shadowRadius = 5.0
layer.shadowOpacity = 0.5
super.backgroundColor = UIColor.clear
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setup()
}
override func draw(_ rect: CGRect) {
customBackgroundColor.setFill()
UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius ?? 0).fill()
let borderRect = bounds.insetBy(dx: borderWidth/2, dy: borderWidth/2)
let borderPath = UIBezierPath(roundedRect: borderRect, cornerRadius: cornerRadius - borderWidth/2)
borderColor.setStroke()
borderPath.lineWidth = borderWidth
borderPath.stroke()
// whatever else you need drawn
}
}
Objective-C .h
IB_DESIGNABLE
@interface RoundRectView : UIView
@property IBInspectable CGFloat cornerRadius;
@property IBInspectable UIColor *borderColor;
@property IBInspectable CGFloat borderWidth;
@end
Objective-C .m
@interface RoundRectView()
@property UIColor *customBackgroundColor;
@end
@implementation RoundRectView
-(void)setup{
self.layer.shadowColor = [UIColor blackColor].CGColor;
self.layer.shadowOffset = CGSizeZero;
self.layer.shadowRadius = 5.0;
self.layer.shadowOpacity = 0.5;
[super setBackgroundColor:[UIColor clearColor]];
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setup];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self setup];
}
return self;
}
-(void)setBackgroundColor:(UIColor *)backgroundColor{
self.customBackgroundColor = backgroundColor;
super.backgroundColor = [UIColor clearColor];
}
-(void)drawRect:(CGRect)rect{
[self.customBackgroundColor setFill];
[[UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:self.cornerRadius] fill];
CGFloat borderInset = self.borderWidth/2;
CGRect borderRect = CGRectInset(self.bounds, borderInset, borderInset);
UIBezierPath *borderPath = [UIBezierPath bezierPathWithRoundedRect:borderRect cornerRadius:self.cornerRadius - borderInset];
[self.borderColor setStroke];
borderPath.lineWidth = self.borderWidth;
[borderPath stroke];
// whatever else you need drawn
}
@end
Here's the swift3 version of Hodit's answer, I had to use it and found it over here and did general corrections for XCode 8. Works like charm!
@IBDesignable class RoundRectView: UIView {
@IBInspectable var cornerRadius: CGFloat = 0.0
@IBInspectable var borderColor: UIColor = UIColor.black
@IBInspectable var borderWidth: CGFloat = 0.5
private var customBackgroundColor = UIColor.white
override var backgroundColor: UIColor?{
didSet {
customBackgroundColor = backgroundColor!
super.backgroundColor = UIColor.clear
}
}
func setup() {
layer.shadowColor = UIColor.black.cgColor;
layer.shadowOffset = CGSize.zero
layer.shadowRadius = 5.0;
layer.shadowOpacity = 0.5;
super.backgroundColor = UIColor.clear
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setup()
}
override func draw(_ rect: CGRect) {
customBackgroundColor.setFill()
UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius ?? 0).fill()
let borderRect = bounds.insetBy(dx: borderWidth/2, dy: borderWidth/2)
let borderPath = UIBezierPath(roundedRect: borderRect, cornerRadius: cornerRadius - borderWidth/2)
borderColor.setStroke()
borderPath.lineWidth = borderWidth
borderPath.stroke()
// whatever else you need drawn
}
}
UIView
in your tableViewCell
and inherit its class from RoundRectView. I have not tried it, though it should work.
I find the following link helpful to understand setting the dropshadow:
How to add a shadow to a UIView
https://i.stack.imgur.com/LcPID.png
SWIFT 3 Solution
Adapted from Mundi's answer
class MyView : UIView {
override func draw(_ rect: CGRect) {
let c = UIGraphicsGetCurrentContext()
c!.addRect(CGRect(x: 10, y: 10, width: 80, height: 80))
c!.setStrokeColor(UIColor.red.cgColor)
c!.strokePath()
}
}
let superview = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
let shadowView = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 100))
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOffset = CGSize.zero
shadowView.layer.shadowOpacity = 0.5
shadowView.layer.shadowRadius = 5
let view = MyView(frame: shadowView.bounds)
view.backgroundColor = UIColor.white
view.layer.cornerRadius = 10.0
view.layer.borderColor = UIColor.gray.cgColor
view.layer.borderWidth = 0.5
view.clipsToBounds = true
shadowView.addSubview(view)
superview.addSubview(shadowView)
Swift 3
I made an UIView extension and its basically the same idea suggested by Mundi:
extension UIView {
func addShadowView() {
//Remove previous shadow views
superview?.viewWithTag(119900)?.removeFromSuperview()
//Create new shadow view with frame
let shadowView = UIView(frame: frame)
shadowView.tag = 119900
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOffset = CGSize(width: 2, height: 3)
shadowView.layer.masksToBounds = false
shadowView.layer.shadowOpacity = 0.3
shadowView.layer.shadowRadius = 3
shadowView.layer.shadowPath = UIBezierPath(rect: bounds).cgPath
shadowView.layer.rasterizationScale = UIScreen.main.scale
shadowView.layer.shouldRasterize = true
superview?.insertSubview(shadowView, belowSubview: self)
}}
Use:
class MyCVCell: UICollectionViewCell {
@IBOutlet weak var containerView: UIView!
override func awakeFromNib() {
super.awakeFromNib()
}
override func draw(_ rect: CGRect) {
super.draw(rect)
containerView.addShadowView()
}}
https://i.stack.imgur.com/ptR44.png
In Swift 4.1. For making rounded corner of UIView I have created Extension of UIView as follow.
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var viewOuter: UIView!
@IBOutlet weak var viewInner: UIView!
override func viewDidLoad() {
super.viewDidLoad()
viewOuter.backgroundColor = UIColor.clear
viewInner.roundCorners(15.0)
viewOuter.addViewShadow()
}
}
extension UIView {
public func roundCorners(_ cornerRadius: CGFloat) {
self.layer.cornerRadius = cornerRadius
self.clipsToBounds = true
self.layer.masksToBounds = true
}
public func addViewShadow() {
DispatchQueue.main.asyncAfter(deadline: (.now() + 0.2)) {
let shadowLayer = CAShapeLayer()
shadowLayer.path = UIBezierPath(roundedRect: self.bounds, cornerRadius: 15).cgPath
shadowLayer.fillColor = UIColor.white.cgColor
shadowLayer.shadowColor = UIColor.lightGray.cgColor
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowOffset = CGSize(width: 2.6, height: 2.6)
shadowLayer.shadowOpacity = 0.8
shadowLayer.shadowRadius = 8.0
self.layer.insertSublayer(shadowLayer, at: 0)
}
}
}
https://i.stack.imgur.com/y5ke8.png
The solution seems much easier than the problem might suggest. I had this with one of my views and used the core part of @Hodit's answer to get it to work. This is all you need actually:
- (void) drawRect:(CGRect)rect {
// make sure the background is set to a transparent color using IB or code
// e.g.: self.backgroundColor = [UIColor clearColor];
// draw a rounded rect in the view
[[UIColor whiteColor] setFill];
[[UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:5.0] fill];
// apply shadow if you haven't already
self.layer.masksToBounds = NO;
self.layer.shadowColor = [[UIColor blackColor] CGColor];
self.layer.shadowOffset = CGSizeMake(0.0,3.0);
self.layer.shadowRadius= 1.0;
self.layer.shadowOpacity = 0.1;
// more code here
}
Note that this doesn't clip subviews. Anything positioned at 0,0 in the view will overlap the visible top left rounded corner.
I use this extension to UIView
:
Import UIKit
extension UIView {
/// A property that accesses the backing layer's opacity.
@IBInspectable
open var opacity: Float {
get {
return layer.opacity
}
set(value) {
layer.opacity = value
}
}
/// A property that accesses the backing layer's shadow
@IBInspectable
open var shadowColor: UIColor? {
get {
guard let v = layer.shadowColor else {
return nil
}
return UIColor(cgColor: v)
}
set(value) {
layer.shadowColor = value?.cgColor
}
}
/// A property that accesses the backing layer's shadowOffset.
@IBInspectable
open var shadowOffset: CGSize {
get {
return layer.shadowOffset
}
set(value) {
layer.shadowOffset = value
}
}
/// A property that accesses the backing layer's shadowOpacity.
@IBInspectable
open var shadowOpacity: Float {
get {
return layer.shadowOpacity
}
set(value) {
layer.shadowOpacity = value
}
}
/// A property that accesses the backing layer's shadowRadius.
@IBInspectable
open var shadowRadius: CGFloat {
get {
return layer.shadowRadius
}
set(value) {
layer.shadowRadius = value
}
}
/// A property that accesses the backing layer's shadowPath.
@IBInspectable
open var shadowPath: CGPath? {
get {
return layer.shadowPath
}
set(value) {
layer.shadowPath = value
}
}
/// A property that accesses the layer.cornerRadius.
@IBInspectable
open var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set(value) {
layer.cornerRadius = value
}
}
/// A property that accesses the layer.borderWith.
@IBInspectable
open var borderWidth: CGFloat {
get {
return layer.borderWidth
}
set(value) {
layer.borderWidth = value
}
}
/// A property that accesses the layer.borderColor property.
@IBInspectable
open var borderColor: UIColor? {
get {
guard let bcolor = layer.borderColor else {
return nil
}
return UIColor(cgColor: bcolor)
}
set(value) {
layer.borderColor = value?.cgColor
}
}
}
Try this it is work for me...
yourView.layer.shadowColor = UIColor.blackColor().CGColor
yourView.layer.shadowOpacity = 0.5
yourView.layer.shadowOffset = CGSize(width: 3, height: 3)
yourView.layer.shadowRadius = 05
yourView.layer.shadowPath = UIBezierPath(rect: yourView.bounds).CGPath
yourView.layer.shouldRasterize = true
In Swift. What did work for me was adding:
self.noteImage.layer.masksToBounds = false
So, the full code is:
self.noteImage.layer.masksToBounds = false
self.noteImage.layer.shadowColor = UIColor.redColor().CGColor
self.noteImage.layer.shadowOpacity = 0.5
self.noteImage.layer.shadowOffset = CGSize(width: 2, height: 2)
self.noteImage.layer.shadowRadius = 1
self.noteImage.layer.shadowPath = UIBezierPath(rect: noteImage.bounds).CGPath
self.noteImage.layer.shouldRasterize = true
In addition to the Frederic Adda's solution, don't forget to position the view that has shadow with a padding to the superview, where the shadow can be drawn. Otherwise the shadow will be clipped off. I made this mistake in my custom cell, and thought the solution was wrong until I added a padding of 8px all around.
This is my solution. If you have multiple type of views like UIView, UIControl, UITableView and so on, and don't want to make subclasses of each of them, or you want to add this effect with smallest changes to your code, then this might be what you are looking for.
Objective-C.h
#import <UIKit/UIKit.h>
@interface UIView (CornerAndShadow)
- (void)setCornerAndShadow;
@end
Objective-C.m
#import "UIView+CornerAndShadow.h"
#import <Masonry.h>
@implementation UIView (CornerAndShadow)
- (void)setCornerAndShadow {
// constants
CGFloat fCornerRadius = 9.f;
// only work for views with superview
if (self.superview == nil) {
return;
}
// set corner
self.layer.cornerRadius = fCornerRadius;
self.layer.masksToBounds = YES;
// create and configure shadowView
UIView *shadowView = [UIView new];
shadowView.backgroundColor = self.backgroundColor; // just to make shadow visible
shadowView.layer.cornerRadius = fCornerRadius;
shadowView.layer.shadowColor = [UIColor redColor].CGColor;
shadowView.layer.shadowOffset = CGSizeMake(0, 3.f);
shadowView.layer.shadowOpacity = 0.5f;
shadowView.layer.shadowRadius = 5.f;
// put shadowView into superview right below self
[self.superview insertSubview:shadowView belowSubview:self];
// set shadowView's frame equal to self
[shadowView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
// use this if you're not using autolayout, and can get real frame here
// shadowView.frame = self.frame;
}
@end
This is an older question, but I would have just done everything in your custom draw method like below.
I usually will do this if I know I want to apply a drop shadow to my rounded view (which of course means I don't want to use masksToBounds
)
You also don't have to add an extra "shadow view" to the hierarchy.
@IBDesignable
class RoundedView: UIView {
@IBInspectable
var cornerRadius: CGFloat = 0
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// You could use custom IBInspectable attributes
// for the stroke and fill color.
context.setFillColor(UIColor.white.cgColor)
context.setStrokeColor(UIColor.orange.cgColor)
// Add a clipping path to get the rounded look
// you want.
UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).addClip()
// Fill and stroke your background.
let background = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
background.lineWidth = 2
background.fill()
background.stroke()
}
private func shadow() {
layer.shadowColor = UIColor.black.cgColor
layer.shadowRadius = 5
layer.shadowOpacity = 0.5
layer.shadowOffset = CGSize.zero
}
override func awakeFromNib() {
super.awakeFromNib()
shadow()
}
}
you can use this function for your all views.
extension UIView{
func radiusAndBorder(radius:CGFloat, color:UIColor = UIColor.clear) -> UIView{
var rounfView:UIView = self
rounfView.layer.cornerRadius = CGFloat(radius)
rounfView.layer.borderWidth = 1
rounfView.layer.borderColor = color.cgColor
rounfView.clipsToBounds = true
return rounfView
}
}