UIView avec ombre, coins arrondis et drawRect personnalisé
Je dois créer un UIView
personnalisé qui aura des coins ronds, une bordure, une ombre et sa méthode drawRect()
est remplacée pour fournir un code de dessin personnalisé avec lequel plusieurs lignes droites sont dessinées dans la vue (je dois utiliser une approche rapide et légère ici car beaucoup de ces vues peuvent être rendues).
Le problème auquel je suis actuellement confronté est que l'ombre ne s'applique plus aux coins ronds dès que je remplace drawRect()
dans la classe view (même sans code personnalisé encore dans il). Voir l'image ci-jointe pour la différence:
Dans le contrôleur de vue, j'utilise le code suivant:
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;
Dans le drawContext()
remplacé, j'utiliserais quelque chose comme:
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);
Mais comme indiqué ci-dessus, le problème de l'ombre se produit même sans ce code ajouté.
Existe-t-il un autre/meilleur moyen de dessiner des éléments légers sur une vue autre que cette approche compatible avec les coins ronds et les ombres? Je ne veux pas ajouter de vues supplémentaires inutiles ou des contextes d'image à la vue car ceux-ci doivent être légers et performants.
16 réponses
C'est délicat. UIView
's clipsToBounds
est nécessaire pour obtenir des coins arrondis. Mais CALayer
's masksToBounds
doit être false
, donc l'ombre est visible. D'une certaine manière, tout fonctionne si drawRect
n'est pas remplacé, mais en fait il ne devrait pas.
La solution consiste à créer un superview pour fournir l'ombre (dans la démonstration ci-dessous c'est le shadowView
). Vous pouvez tester ce qui suit dans 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)
Résultat:
J'ai écrit Une petite extension à UIView pour gérer les coins arrondis et l'ombre portée. Comme les variables sont @IBInspectable, tout peut être réglé directement dans le 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
}
}
Et voici à quoi cela ressemble dans le storyboard :
Il y a une exigence: Ne touchez ni clipToBounds sur la vue (dans le code ou dans IB) ni masksToBound sur le calque.
NB: un cas dans lequel cela ne fonctionnera pas : tableViews.
Comme UITableView déclenche automatiquement clipToBounds
sous le capot, nous ne pouvons pas avoir d'ombre portée.
EDIT: comme Claudia Fitero l'a bien remarqué, vous devez laisser un petit remplissage autour de la vue à laquelle vous ajoutez une ombre, sinon l'ombre ne sera pas visible. Un rembourrage 2px suffit généralement (en fonction de votre rayon d'ombre).
L'ombre est supprimée de tout ce qui se trouve à l'intérieur du calque de la vue. Lorsque vous désactivez l'écrêtage, le rectangle de calque entier est rempli par défaut backgroundColor
de sorte que l'ombre devient également rectangulaire. Au lieu de l'écrêter avec un masque arrondi, faites simplement arrondir le contenu du calque, dessinez-le vous-même. Et la bordure de layer
est dessinée autour de ses limites, vous devez donc la dessiner vous-même.
Par exemple, dans backgroundColor
setter, définissez la couleur d'arrière-plan réelle sur clearColor
et utilisez la couleur passée dans drawRect
pour dessiner un rect arrondi avec.
Dans l'exemple ci-dessous, je déclare les propriétés comme IBInspectable
et toute la classe comme IBDesignable
, donc tout peut être défini dans storyboard. De cette façon, vous pouvez même utiliser le sélecteur d'arrière-plan par défaut pour changer votre couleur rect arrondie.
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
}
}
Objectif-C.h
IB_DESIGNABLE
@interface RoundRectView : UIView
@property IBInspectable CGFloat cornerRadius;
@property IBInspectable UIColor *borderColor;
@property IBInspectable CGFloat borderWidth;
@end
Objectif-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
Voici la version swift3 de la réponse de Hodit , j'ai dû l'utiliser et l'ai trouvée ici et j'ai fait des corrections générales pour XCode 8. Fonctionne comme le charme!
@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
}
}
Swift 3
J'ai fait une extension UIView et c'est fondamentalement la même idée suggérée par 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)
}}
Utilisation:
class MyCVCell: UICollectionViewCell {
@IBOutlet weak var containerView: UIView!
override func awakeFromNib() {
super.awakeFromNib()
}
override func draw(_ rect: CGRect) {
super.draw(rect)
containerView.addShadowView()
}}
Solution SWIFT 3
Adapté de la réponse de Mundi
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)
Je trouve le lien suivant utile pour comprendre le réglage du dropshadow:
Comment ajouter une ombre à un UIView
Pour définir le coin rond pour UIVIEW, il suffit de définir la valeur layer.cornerRadius
dans Interface builder, veuillez vérifier la capture d'écran.
La solution semble beaucoup plus facile que le problème pourrait suggérer. J'ai eu ceci avec un de mes points de vue et utilisé la partie centrale de la réponse de @Hodit pour le faire fonctionner. C'est tout ce dont vous avez besoin en fait:
- (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
}
Notez que cela ne coupe pas les sous-vues. Tout ce qui est positionné à 0,0 dans la vue chevauchera le coin arrondi supérieur gauche visible.
Essayez ceci c'est du travail pour moi...
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
Dans Swift. Ce qui a fonctionné pour moi a été d'ajouter:
self.noteImage.layer.masksToBounds = false
Donc, le code complet est:
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
En plus de la solution de Frederic Adda, n'oubliez pas de positionner la vue qui a shadow avec un remplissage à la superview, où l'ombre peut être dessinée. Sinon, l'ombre sera coupée. J'ai fait cette erreur dans ma cellule personnalisée, et je pensais que la solution était fausse jusqu'à ce que j'ajoute un remplissage de 8px tout autour.
C'est ma solution. Si vous avez plusieurs types de vues comme UIView, UIControl, UITableView et ainsi de suite, et que vous ne voulez pas créer de sous-classes de chacune d'elles, ou si vous voulez ajouter cet effet avec les plus petites modifications à votre code, alors c'est peut-être ce que vous recherchez.
Objectif-C. H
#import <UIKit/UIKit.h>
@interface UIView (CornerAndShadow)
- (void)setCornerAndShadow;
@end
Objectif-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
J'utilise cette extension 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 v = layer.borderColor else {
return nil
}
return UIColor(cgColor: v)
}
set(value) {
layer.borderColor = value?.cgColor
}
}
}
C'est une question plus ancienne, mais j'aurais tout fait dans votre méthode de tirage personnalisée comme ci-dessous.
Je le ferai généralement si je sais que je veux appliquer une ombre portée à ma vue arrondie (ce qui signifie Bien sûr que je ne veux pas utiliser masksToBounds
)
Vous n'avez pas non plus besoin d'ajouter une "vue d'ombre" supplémentaire à la hiérarchie.
@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()
}
}
Dans Swift 4.1. Pour faire le coin arrondi de UIView j'ai créé L'Extension de UIView comme suit.
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)
}
}
}
Vous pouvez utiliser cette fonction pour toutes vos vues.
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
}
}