Comment dessiner une ombre sous une UIView?

352

J'essaie de dessiner une ombre sous le bord inférieur d'un UIViewCocoa Touch. Je comprends que je devrais utiliser CGContextSetShadow()pour dessiner l'ombre, mais le guide de programmation Quartz 2D est un peu vague:

  1. Enregistrez l'état graphique.
  2. Appelez la fonction CGContextSetShadowen passant les valeurs appropriées.
  3. Effectuez tous les dessins auxquels vous souhaitez appliquer des ombres.
  4. Restaurer l'état graphique

J'ai essayé ce qui suit dans une UIViewsous - classe:

- (void)drawRect:(CGRect)rect {
    CGContextRef currentContext = UIGraphicsGetCurrentContext();
    CGContextSaveGState(currentContext);
    CGContextSetShadow(currentContext, CGSizeMake(-15, 20), 5);
    CGContextRestoreGState(currentContext);
    [super drawRect: rect];
}

..mais cela ne fonctionne pas pour moi et je suis un peu coincé sur (a) où aller ensuite et (b) s'il y a quelque chose que je dois faire UIViewpour que ça marche?

Fraser Speirs
la source

Réponses:

98

Dans votre code actuel, vous enregistrez le GStatecontexte actuel, le configurez pour dessiner une ombre .. et le restaurez à ce qu'il était avant de le configurer pour dessiner une ombre. Puis, enfin, vous invoquez la mise en œuvre de la superclasse de drawRect:.

Tout dessin qui devrait être affecté par le paramètre d'ombre doit se produire après

CGContextSetShadow(currentContext, CGSizeMake(-15, 20), 5);

mais avant

CGContextRestoreGState(currentContext);

Donc, si vous voulez que la superclasse drawRect:soit «enveloppée» dans une ombre, alors que diriez-vous si vous réorganisez votre code comme ça?

- (void)drawRect:(CGRect)rect {
    CGContextRef currentContext = UIGraphicsGetCurrentContext();
    CGContextSaveGState(currentContext);
    CGContextSetShadow(currentContext, CGSizeMake(-15, 20), 5);
    [super drawRect: rect];
    CGContextRestoreGState(currentContext);
}
Christian Brunschen
la source
789

Une approche beaucoup plus simple consiste à définir certains attributs de couche de la vue lors de l'initialisation:

self.layer.masksToBounds = NO;
self.layer.shadowOffset = CGSizeMake(-15, 20);
self.layer.shadowRadius = 5;
self.layer.shadowOpacity = 0.5;

Vous devez importer QuartzCore.

#import <QuartzCore/QuartzCore.h>
ollie
la source
4
Mais sachez que cela ne fonctionne que sur iOS 3.2+, donc si votre application fonctionne sur une ancienne version, vous devez utiliser la solution de Christian ou une image statique derrière la vue si c'est une option.
florianbuerger
119
Cette solution nécessite également l'ajout #import <QuartzCore/QuartzCore.h>"au fichier .h.
MusiGenesis
26
Réglage masksToBoundsde NOannulerait la cornerRadius, non?
pixelfreak
4
D'accord, pour résoudre ce problème, la couleur d'arrière-plan doit être définie sur le calque et la vue doit être transparente.
pixelfreak
@pixelfreak Comment faites-vous cela? J'ai essayé self.layer.backgroundColor = [[UIColor whiteColor] CGColor];mais pas de chance. Quelle vue doit être transparente?
Victor Van Hee
232
self.layer.masksToBounds = NO;
self.layer.cornerRadius = 8; // if you like rounded corners
self.layer.shadowOffset = CGSizeMake(-15, 20);
self.layer.shadowRadius = 5;
self.layer.shadowOpacity = 0.5;

Cela ralentira l'application. L'ajout de la ligne suivante peut améliorer les performances tant que votre vue est visiblement rectangulaire:

self.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.bounds].CGPath;
ZYiOS
la source
1
Il est probablement intéressant de noter que cette optimisation n'est utile que si votre vue est visiblement rectangulaire.
Benjamin Dobell
1
self.layer.shadowPath ... au lieu de quoi? ou simplement en y ajoutant
Chrizzz
2
Ajoutez simplement cette ligne supplémentaire en plus des autres.
christophercotton
11
@NathanGaskin - dessiner des ombres est une opération coûteuse, donc par exemple si votre application autorise une autre orientation d'interface et que vous commencez à faire pivoter l'appareil, sans spécifier explicitement le chemin de l'ombre, elle doit être rendue plusieurs fois pendant cette animation qui le fera, selon la forme, ralentit probablement visiblement l'animation
Peter Pajchl
9
@BenjaminDobell Cette ligne particulière ne fonctionne que pour les rectangles mais vous pouvez également créer des chemins non rectangulaires. Par exemple, si vous avez un rectangle arrondi, vous pouvez utiliser bezierPathWithRoundedRect: cornerRadius:
Dan Dyer
160

Même solution, mais juste pour vous rappeler: vous pouvez définir l'ombre directement dans le storyboard.

Ex:

entrez la description de l'image ici

Antzi
la source
2
Malheureusement, je pense que CGColor est hors de portée du storyboard :(
Antzi
1
il suffit de définir une catégorie pour UIViewou CGLayerqui est un UIColorwrapper pour la CGColorpropriété;)
DeFrenZ
1
Ce lien décrit comment procéder: cocoadventures.org/post/104137872754/…
Kamchatka
8
Pour faciliter le copier-coller pour les utilisateurs: layer.masksToBound, layer.shadowOffset, layer.shadowRadius, layer.shadowOpacity. Les fautes de frappe vous tueront ici.
etayluz
3
La valeur layer.shadowOpacity doit être de 0,5 et non de 0,5
Desert Rose
43

Vous pouvez essayer ceci .... vous pouvez jouer avec les valeurs. Le shadowRadiusdicte la quantité de flou. shadowOffsetdicte où va l'ombre.

Swift 2.0

let radius: CGFloat = demoView.frame.width / 2.0 //change it to .height if you need spread for height
let shadowPath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 2.1 * radius, height: demoView.frame.height))
//Change 2.1 to amount of spread you need and for height replace the code for height

demoView.layer.cornerRadius = 2
demoView.layer.shadowColor = UIColor.blackColor().CGColor
demoView.layer.shadowOffset = CGSize(width: 0.5, height: 0.4)  //Here you control x and y
demoView.layer.shadowOpacity = 0.5
demoView.layer.shadowRadius = 5.0 //Here your control your blur
demoView.layer.masksToBounds =  false
demoView.layer.shadowPath = shadowPath.CGPath

Swift 3.0

let radius: CGFloat = demoView.frame.width / 2.0 //change it to .height if you need spread for height 
let shadowPath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 2.1 * radius, height: demoView.frame.height)) 
//Change 2.1 to amount of spread you need and for height replace the code for height

demoView.layer.cornerRadius = 2
demoView.layer.shadowColor = UIColor.black.cgColor
demoView.layer.shadowOffset = CGSize(width: 0.5, height: 0.4)  //Here you control x and y
demoView.layer.shadowOpacity = 0.5
demoView.layer.shadowRadius = 5.0 //Here your control your blur
demoView.layer.masksToBounds =  false
demoView.layer.shadowPath = shadowPath.cgPath

Exemple avec propagation

Exemple avec propagation

Pour créer une ombre de base

    demoView.layer.cornerRadius = 2
    demoView.layer.shadowColor = UIColor.blackColor().CGColor
    demoView.layer.shadowOffset = CGSizeMake(0.5, 4.0); //Here your control your spread
    demoView.layer.shadowOpacity = 0.5 
    demoView.layer.shadowRadius = 5.0 //Here your control your blur

Exemple d'ombre de base dans Swift 2.0

PRODUCTION

O-mkar
la source
19

Solution simple et propre utilisant Interface Builder

Ajoutez un fichier nommé UIView.swift dans votre projet (ou collez-le simplement dans n'importe quel fichier):

import UIKit

@IBDesignable extension UIView {

    /* The color of the shadow. Defaults to opaque black. Colors created
    * from patterns are currently NOT supported. Animatable. */
    @IBInspectable var shadowColor: UIColor? {
        set {
            layer.shadowColor = newValue!.CGColor
        }
        get {
            if let color = layer.shadowColor {
                return UIColor(CGColor:color)
            }
            else {
                return nil
            }
        }
    }

    /* The opacity of the shadow. Defaults to 0. Specifying a value outside the
    * [0,1] range will give undefined results. Animatable. */
    @IBInspectable var shadowOpacity: Float {
        set {
            layer.shadowOpacity = newValue
        }
        get {
            return layer.shadowOpacity
        }
    }

    /* The shadow offset. Defaults to (0, -3). Animatable. */
    @IBInspectable var shadowOffset: CGPoint {
        set {
            layer.shadowOffset = CGSize(width: newValue.x, height: newValue.y)
        }
        get {
            return CGPoint(x: layer.shadowOffset.width, y:layer.shadowOffset.height)
        }
    }

    /* The blur radius used to create the shadow. Defaults to 3. Animatable. */
    @IBInspectable var shadowRadius: CGFloat {
        set {
            layer.shadowRadius = newValue
        }
        get {
            return layer.shadowRadius
        }
    }
}

Ensuite, cela sera disponible dans Interface Builder pour chaque vue dans le panneau Utilitaires> Inspecteur d'attributs:

Panneau Utilitaires

Vous pouvez facilement définir l'ombre maintenant.

Remarques:
- L'ombre n'apparaîtra pas dans IB, uniquement lors de l'exécution.
- Comme l'a dit Mazen Kasser

Pour ceux qui n'ont pas réussi à faire [...] fonctionner cela, assurez-vous que Clip Subviews ( clipsToBounds) n'est pas activé

Axel Guilmin
la source
Ceci est la bonne réponse - configuration sur le code pour les futures interfaces
Crake
Cette solution m'amène à un comportement non fonctionnel avec le message d'avertissement suivant (un pour chaque propriété):Failed to set (shadowColor) user defined inspected property on (UICollectionViewCell): [<UICollectionViewCell> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key shadowColor.
Xvolks
1
J'ai dû importer UIKitpour le faire fonctionner, l' Foundationimportation de base créée par XCode n'est pas suffisante, mais se compile très bien. J'aurais dû copier tout le code source. Merci Axel pour cette belle solution.
Xvolks
13

Je l'utilise dans le cadre de mes utils. Avec cela, nous pouvons non seulement définir l'ombre, mais également obtenir un coin arrondi pour tout UIView. Vous pouvez également définir la couleur que vous préférez. Normalement, le noir est préféré, mais parfois, lorsque l'arrière-plan n'est pas blanc, vous voudrez peut-être autre chose. Voici ce que j'utilise -

in utils.m
+ (void)roundedLayer:(CALayer *)viewLayer 
              radius:(float)r 
              shadow:(BOOL)s
{
    [viewLayer setMasksToBounds:YES];
    [viewLayer setCornerRadius:r];        
    [viewLayer setBorderColor:[RGB(180, 180, 180) CGColor]];
    [viewLayer setBorderWidth:1.0f];
    if(s)
    {
        [viewLayer setShadowColor:[RGB(0, 0, 0) CGColor]];
        [viewLayer setShadowOffset:CGSizeMake(0, 0)];
        [viewLayer setShadowOpacity:1];
        [viewLayer setShadowRadius:2.0];
    }
    return;
}

Pour l'utiliser, nous devons appeler cela - [utils roundedLayer:yourview.layer radius:5.0f shadow:YES];

Srikar Appalaraju
la source
7

Swift 3

extension UIView {
    func installShadow() {
        layer.cornerRadius = 2
        layer.masksToBounds = false
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowOffset = CGSize(width: 0, height: 1)
        layer.shadowOpacity = 0.45
        layer.shadowPath = UIBezierPath(rect: bounds).cgPath
        layer.shadowRadius = 1.0
    }
}
neoneye
la source
si vous utilisiez cette méthode,
Josh O'Connor
Cela ne me donne pas un coin arrondi, est-ce que je fais quelque chose de mal?
Nikhil Manapure
@NikhilManapure rien ne se passe, cela pourrait être dû au fait que la fonction installShadow()n'est pas invoquée depuis viewDidLoad()ouviewWillAppear()
neoneye
Je l'appelais de la drawméthode de vue surchargée . L'ombre arrive correctement, mais pas les coins arrondis.
Nikhil Manapure
@NikhilManapure pas de bordures arrondies, cela peut être dû au fait que la vue est une UIStackViewqui ne fait que la mise en page et n'a pas de vue. Vous devrez peut-être insérer un régulier UIViewcomme conteneur pour tout.
neoneye
6

Si vous souhaitez utiliser StoryBoard et ne souhaitez pas continuer à taper des attributs d'exécution, vous pouvez facilement créer une extension pour les vues et les rendre utilisables dans le storyboard.

Étape 1. créer une extension

extension UIView {

@IBInspectable var shadowRadius: CGFloat {
    get {
        return layer.shadowRadius
    }
    set {
        layer.shadowRadius = newValue
    }
}

@IBInspectable var shadowOpacity: Float {
    get {
        return layer.shadowOpacity
    }
    set {
        layer.shadowOpacity = newValue
    }
}

@IBInspectable var shadowOffset: CGSize {
    get {
        return layer.shadowOffset
    }
    set {
        layer.shadowOffset = newValue
    }
}

@IBInspectable var maskToBound: Bool {
    get {
        return layer.masksToBounds
    }
    set {
        layer.masksToBounds = newValue
    }
}
}

étape 2. vous pouvez maintenant utiliser ces attributs dans le storyboardimage de storyboard

king_T
la source
3

Pour ceux qui n'ont pas réussi à faire fonctionner cela (comme moi!) Après avoir essayé toutes les réponses ici, assurez-vous simplement que Clip Subviews n'est pas activé dans l'inspecteur des attributs ...

Mazen Kasser
la source
1

Swift 3

self.paddingView.layer.masksToBounds = false
self.paddingView.layer.shadowOffset = CGSize(width: -15, height: 10)
self.paddingView.layer.shadowRadius = 5
self.paddingView.layer.shadowOpacity = 0.5
Josh O'Connor
la source
1

Vous pouvez utiliser ma fonction utilitaire créée pour l'ombre et le rayon d'angle comme ci-dessous:

- (void)addShadowWithRadius:(CGFloat)shadowRadius withShadowOpacity:(CGFloat)shadowOpacity withShadowOffset:(CGSize)shadowOffset withShadowColor:(UIColor *)shadowColor withCornerRadius:(CGFloat)cornerRadius withBorderColor:(UIColor *)borderColor withBorderWidth:(CGFloat)borderWidth forView:(UIView *)view{

    // drop shadow
    [view.layer setShadowRadius:shadowRadius];
    [view.layer setShadowOpacity:shadowOpacity];
    [view.layer setShadowOffset:shadowOffset];
    [view.layer setShadowColor:shadowColor.CGColor];

    // border radius
    [view.layer setCornerRadius:cornerRadius];

    // border
    [view.layer setBorderColor:borderColor.CGColor];
    [view.layer setBorderWidth:borderWidth];
}

J'espère que cela vous aidera !!!

Sandip Patel - SM
la source
1

Tous Répondez bien mais je veux ajouter un point

Si vous rencontrez un problème lorsque vous avez des cellules de tableau, dequez une nouvelle cellule il y a un décalage dans l'ombre donc dans ce cas, vous devez placer votre code d'ombre dans une méthode layoutSubviews afin qu'il se comporte bien dans toutes les conditions.

-(void)layoutSubviews{
    [super layoutSubviews];

    [self.contentView setNeedsLayout];
    [self.contentView layoutIfNeeded];
    [VPShadow applyShadowView:self];
}

ou dans ViewControllers pour une vue spécifique, placez du code fantôme à l'intérieur de la méthode suivante afin qu'il fonctionne bien

-(void)viewDidLayoutSubviews{
    [super viewDidLayoutSubviews];

    [self.viewShadow layoutIfNeeded];
    [VPShadow applyShadowView:self.viewShadow];
}

J'ai modifié mon implémentation shadow pour les nouveaux développeurs pour une forme plus généralisée ex:

/*!
 @brief Add shadow to a view.

 @param layer CALayer of the view.

 */
+(void)applyShadowOnView:(CALayer *)layer OffsetX:(CGFloat)x OffsetY:(CGFloat)y blur:(CGFloat)radius opacity:(CGFloat)alpha RoundingCorners:(CGFloat)cornerRadius{
    UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRoundedRect:layer.bounds cornerRadius:cornerRadius];
    layer.masksToBounds = NO;
    layer.shadowColor = [UIColor blackColor].CGColor;
    layer.shadowOffset = CGSizeMake(x,y);// shadow x and y
    layer.shadowOpacity = alpha;
    layer.shadowRadius = radius;// blur effect
    layer.shadowPath = shadowPath.CGPath;
}
Vishal16
la source
1

Pour les autres Xamariens, la version Xamarin.iOS / C # de la réponse ressemblerait à ceci:

public override void DrawRect(CGRect area, UIViewPrintFormatter formatter)
{
    CGContext currentContext = UIGraphics.GetCurrentContext();
    currentContext.SaveState();
    currentContext.SetShadow(new CGSize(-15, 20), 5);
    base.DrawRect(area, formatter);
    currentContext.RestoreState();                
}

La principale différence est que vous acquérez une instance CGContextsur laquelle vous appelez directement les méthodes appropriées.

Martin Zikmund
la source
1

Vous pouvez l'utiliser Extensionpour ajouter une ombre

extension UIView {

    func addShadow(offset: CGSize, color: UIColor, radius: CGFloat, opacity: Float)
    {
        layer.masksToBounds = false
        layer.shadowOffset = offset
        layer.shadowColor = color.cgColor
        layer.shadowRadius = radius
        layer.shadowOpacity = opacity

        let backgroundCGColor = backgroundColor?.cgColor
        backgroundColor = nil
        layer.backgroundColor =  backgroundCGColor
    }
}

vous pouvez l'appeler comme

your_Custom_View.addShadow(offset: CGSize(width: 0, height: 1), color: UIColor.black, radius: 2.0, opacity: 1.0)
pigeon_39
la source
0

Esquisser l'ombre à l'aide d'IBDesignable et IBInspectable dans Swift 4

COMMENT L'UTILISER

DEMO

CROQUIS ET XCODE SIDE BY SIDE

Shadow Exampl

CODE

@IBDesignable class ShadowView: UIView {

    @IBInspectable var shadowColor: UIColor? {
        get {
            if let color = layer.shadowColor {
                return UIColor(cgColor: color)
            }
            return nil
        }
        set {
            if let color = newValue {
                layer.shadowColor = color.cgColor
            } else {
                layer.shadowColor = nil
            }
        }
    }

    @IBInspectable var shadowOpacity: Float {
        get {
            return layer.shadowOpacity
        }
        set {
            layer.shadowOpacity = newValue
        }
    }

    @IBInspectable var shadowOffset: CGPoint {
        get {
            return CGPoint(x: layer.shadowOffset.width, y:layer.shadowOffset.height)
        }
        set {
            layer.shadowOffset = CGSize(width: newValue.x, height: newValue.y)
        }

     }

    @IBInspectable var shadowBlur: CGFloat {
        get {
            return layer.shadowRadius
        }
        set {
            layer.shadowRadius = newValue / 2.0
        }
    }

    @IBInspectable var shadowSpread: CGFloat = 0 {
        didSet {
            if shadowSpread == 0 {
                layer.shadowPath = nil
            } else {
                let dx = -shadowSpread
                let rect = bounds.insetBy(dx: dx, dy: dx)
                layer.shadowPath = UIBezierPath(rect: rect).cgPath
            }
        }
    }
}

PRODUCTION

SORTIE DEMO

O-mkar
la source
Vous pouvez améliorer le code en supprimant les préfixes cachés des attributs. L'objectif de ShadowView est clair sur le fonctionnement de l'ombre. Une autre chose est que vous pouvez ajouter un rayon de coin à cette classe. (Aujourd'hui, la plupart des vues d'ombre impliquent un rayon de coin)
mathema