¿Efecto de sombra interior en la capa UIView?

92

Tengo el siguiente CALayer:

CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = CGRectMake(8, 57, 296, 30);
gradient.cornerRadius = 3.0f;
gradient.colors = [NSArray arrayWithObjects:(id)[RGB(130, 0, 140) CGColor], (id)[RGB(108, 0, 120) CGColor], nil];
[self.layer insertSublayer:gradient atIndex:0];

Me gustaría agregar una sombra interior efecto de , pero no estoy muy seguro de cómo hacerlo. Supongo que tendría que dibujar en drawRect, sin embargo, esto agregaría la capa encima de otros objetos UIView, ya que se supone que es una barra detrás de algunos botones, así que no sé qué hacer.

Podría agregar otra capa, pero nuevamente, no estoy seguro de cómo lograr el efecto de sombra interna (como este:

ingrese la descripción de la imagen aquí

Ayuda apreciada ...

runmad
fuente

Respuestas:

108

Para cualquiera que se pregunte cómo dibujar una sombra interior usando Core Graphics según la sugerencia de Costique, entonces así es como: (en iOS, ajuste según sea necesario)

En su método drawRect: ...

CGRect bounds = [self bounds];
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat radius = 0.5f * CGRectGetHeight(bounds);


// Create the "visible" path, which will be the shape that gets the inner shadow
// In this case it's just a rounded rect, but could be as complex as your want
CGMutablePathRef visiblePath = CGPathCreateMutable();
CGRect innerRect = CGRectInset(bounds, radius, radius);
CGPathMoveToPoint(visiblePath, NULL, innerRect.origin.x, bounds.origin.y);
CGPathAddLineToPoint(visiblePath, NULL, innerRect.origin.x + innerRect.size.width, bounds.origin.y);
CGPathAddArcToPoint(visiblePath, NULL, bounds.origin.x + bounds.size.width, bounds.origin.y, bounds.origin.x + bounds.size.width, innerRect.origin.y, radius);
CGPathAddLineToPoint(visiblePath, NULL, bounds.origin.x + bounds.size.width, innerRect.origin.y + innerRect.size.height);
CGPathAddArcToPoint(visiblePath, NULL,  bounds.origin.x + bounds.size.width, bounds.origin.y + bounds.size.height, innerRect.origin.x + innerRect.size.width, bounds.origin.y + bounds.size.height, radius);
CGPathAddLineToPoint(visiblePath, NULL, innerRect.origin.x, bounds.origin.y + bounds.size.height);
CGPathAddArcToPoint(visiblePath, NULL,  bounds.origin.x, bounds.origin.y + bounds.size.height, bounds.origin.x, innerRect.origin.y + innerRect.size.height, radius);
CGPathAddLineToPoint(visiblePath, NULL, bounds.origin.x, innerRect.origin.y);
CGPathAddArcToPoint(visiblePath, NULL,  bounds.origin.x, bounds.origin.y, innerRect.origin.x, bounds.origin.y, radius);
CGPathCloseSubpath(visiblePath);

// Fill this path
UIColor *aColor = [UIColor redColor];
[aColor setFill];
CGContextAddPath(context, visiblePath);
CGContextFillPath(context);


// Now create a larger rectangle, which we're going to subtract the visible path from
// and apply a shadow
CGMutablePathRef path = CGPathCreateMutable();
//(when drawing the shadow for a path whichs bounding box is not known pass "CGPathGetPathBoundingBox(visiblePath)" instead of "bounds" in the following line:)
//-42 cuould just be any offset > 0
CGPathAddRect(path, NULL, CGRectInset(bounds, -42, -42));

// Add the visible path (so that it gets subtracted for the shadow)
CGPathAddPath(path, NULL, visiblePath);
CGPathCloseSubpath(path);

// Add the visible paths as the clipping path to the context
CGContextAddPath(context, visiblePath); 
CGContextClip(context);         


// Now setup the shadow properties on the context
aColor = [UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.5f];
CGContextSaveGState(context);
CGContextSetShadowWithColor(context, CGSizeMake(0.0f, 1.0f), 3.0f, [aColor CGColor]);   

// Now fill the rectangle, so the shadow gets drawn
[aColor setFill];   
CGContextSaveGState(context);   
CGContextAddPath(context, path);
CGContextEOFillPath(context);

// Release the paths
CGPathRelease(path);    
CGPathRelease(visiblePath);

Entonces, esencialmente existen los siguientes pasos:

  1. Crea tu camino
  2. Establezca el color de relleno que desee, agregue esta ruta al contexto y complete el contexto
  3. Ahora cree un rectángulo más grande que pueda delimitar la ruta visible. Antes de cerrar esta ruta, agregue la ruta visible. Luego cierre la ruta, de modo que cree una forma con la ruta visible restada. Es posible que desee investigar los métodos de relleno (devanado distinto de cero de par / impar) dependiendo de cómo haya creado estas rutas. En esencia, para que los subtrazados se "resten" cuando los suma, debe dibujarlos (o más bien construirlos) en direcciones opuestas, una en sentido horario y otra en sentido antihorario.
  4. Luego, debe establecer su ruta visible como la ruta de recorte en el contexto, para que no dibuje nada fuera de ella en la pantalla.
  5. Luego, configure la sombra en el contexto, que incluye el desplazamiento, el desenfoque y el color.
  6. Luego llene la forma grande con el agujero. El color no importa, porque si has hecho todo bien, no verás este color, solo la sombra.
Daniel Thorpe
fuente
Gracias, pero ¿es posible ajustar el radio? Actualmente se basa en los límites, pero me gustaría basarme en un radio establecido (como 5.0f). Con el código anterior, está demasiado redondeado.
runmad
2
@runmad Bueno, puede crear cualquier tipo de CGPath visible que desee, el ejemplo utilizado aquí es solo eso, un ejemplo, elegido por brevedad. Si desea crear un rectángulo redondeado, puede hacer algo como: CGPath visiblePath = [UIBezierPath bezierPathWithRoundedRect: rect cornerRadius: radius] .CGPath Espero que eso ayude.
Daniel Thorpe
4
@DanielThorpe: +1 por la buena respuesta. Arreglé el código de ruta rect redondeado (el suyo se rompió al cambiar el radio) y simplifiqué el código de ruta rect externo. Espero que no te importe.
Regexident
¿Cómo puedo configurar correctamente la sombra interior desde 4 direcciones, no solo 2?
Protocole
@Protocole puede establecer el desplazamiento en {0,0}, pero use un radio de sombra de, por ejemplo, 4.f.
Daniel Thorpe
47

Sé que llego tarde a esta fiesta, pero esto me habría ayudado a encontrar temprano en mis viajes ...

Para dar crédito a quien se debe el crédito, esto es esencialmente una modificación de la elaboración de Daniel Thorpe sobre la solución de Costique de restar una región más pequeña de una región más grande. Esta versión es para aquellos que usan la composición de capas en lugar de anular-drawRect:

La CAShapeLayerclase se puede utilizar para lograr el mismo efecto:

CAShapeLayer* shadowLayer = [CAShapeLayer layer];
[shadowLayer setFrame:[self bounds]];

// Standard shadow stuff
[shadowLayer setShadowColor:[[UIColor colorWithWhite:0 alpha:1] CGColor]];
[shadowLayer setShadowOffset:CGSizeMake(0.0f, 0.0f)];
[shadowLayer setShadowOpacity:1.0f];
[shadowLayer setShadowRadius:5];

// Causes the inner region in this example to NOT be filled.
[shadowLayer setFillRule:kCAFillRuleEvenOdd];

// Create the larger rectangle path.
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectInset(bounds, -42, -42));

// Add the inner path so it's subtracted from the outer path.
// someInnerPath could be a simple bounds rect, or maybe
// a rounded one for some extra fanciness.
CGPathAddPath(path, NULL, someInnerPath);
CGPathCloseSubpath(path);

[shadowLayer setPath:path];
CGPathRelease(path);

[[self layer] addSublayer:shadowLayer];

En este punto, si su capa principal no se enmascara hasta sus límites, verá el área adicional de la capa de máscara alrededor de los bordes de la capa. Estos serán 42 píxeles de negro si copia el ejemplo directamente. Para deshacerse de él, simplemente puede usar otro CAShapeLayercon el mismo camino y configurarlo como la máscara de la capa de sombra:

CAShapeLayer* maskLayer = [CAShapeLayer layer];
[maskLayer setPath:someInnerPath];
[shadowLayer setMask:maskLayer];

No lo he comparado yo mismo, pero sospecho que usar este enfoque junto con la rasterización es más eficaz que anular -drawRect:.

Matt Wilding
fuente
3
someInnerPath? ¿Podrías explicar eso un poco más por favor?
Moe
4
@Moe Puede ser cualquier CGPath arbitrario que desee. [[UIBezierPath pathWithRect:[shadowLayer bounds]] CGPath]siendo la opción más sencilla.
Matt Wilding
Saludos por eso Matt :-)
Moe
Obtengo un rectángulo negro (exterior) para shadowLayer.path que dibuja correctamente la sombra interior. ¿Cómo puedo deshacerme de él (el rectángulo exterior negro)? Parece que solo puede establecer fillColor dentro de un contexto y no usa uno.
Olivier
11
¡Esto funciona muy bien! Subí a github con algunas adiciones. Pruébelo
inamiy
35

Es posible dibujar una sombra interior con Core Graphics haciendo una ruta rectangular grande fuera de los límites, restando una ruta rectangular del tamaño de los límites y llenando la ruta resultante con una sombra "normal".

Sin embargo, dado que necesita combinarlo con una capa de degradado, creo que una solución más fácil es crear una imagen PNG transparente de 9 partes de la sombra interior y estirarla al tamaño correcto. La imagen de sombra de 9 partes se vería así (su tamaño es de 21x21 píxeles):

texto alternativo

CALayer *innerShadowLayer = [CALayer layer];
innerShadowLayer.contents = (id)[UIImage imageNamed: @"innershadow.png"].CGImage;
innerShadowLayer.contentsCenter = CGRectMake(10.0f/21.0f, 10.0f/21.0f, 1.0f/21.0f, 1.0f/21.0f);

Luego configure el marco de innerShadowLayer y debería estirar la sombra correctamente.

Costique
fuente
Sí, supongo que tienes razón. Solo quería que la capa fuera lo más plana posible. Podría crear la imagen en Photoshop con la sombra interior y el aspecto degradado, solo tengo problemas con los colores que coinciden al 100% en el dispositivo cuando uso una imagen.
runmad
Sí, ese es un problema con todos los degradados y sombras, simplemente no puedo reproducir estos efectos de Photoshop 1: 1 en iOS, por mucho que lo intente.
Costique
30

Una versión simplificada usando solo un CALayer, en Swift:

import UIKit

final class FrameView : UIView {
    init() {
        super.init(frame: CGRect.zero)
        backgroundColor = UIColor.white
    }

    @available(*, unavailable)
    required init?(coder decoder: NSCoder) { fatalError("unavailable") }

    override func layoutSubviews() {
        super.layoutSubviews()
        addInnerShadow()
    }

    private func addInnerShadow() {
        let innerShadow = CALayer()
        innerShadow.frame = bounds
        // Shadow path (1pt ring around bounds)
        let path = UIBezierPath(rect: innerShadow.bounds.insetBy(dx: -1, dy: -1))
        let cutout = UIBezierPath(rect: innerShadow.bounds).reversing()
        path.append(cutout)
        innerShadow.shadowPath = path.cgPath
        innerShadow.masksToBounds = true
        // Shadow properties
        innerShadow.shadowColor = UIColor(white: 0, alpha: 1).cgColor // UIColor(red: 0.71, green: 0.77, blue: 0.81, alpha: 1.0).cgColor
        innerShadow.shadowOffset = CGSize.zero
        innerShadow.shadowOpacity = 1
        innerShadow.shadowRadius = 3
        // Add
        layer.addSublayer(innerShadow)
    }
}

Tenga en cuenta que la capa innerShadow no debe tener un color de fondo opaco, ya que se representará frente a la sombra.

Patrick Pijnappel
fuente
La última línea contiene 'capa'. ¿De donde viene esto?
Charlie Seligman
@CharlieSeligman Es la capa principal, que podría ser cualquier capa. Puede usar una capa personalizada o la capa de la vista (UIView tiene una propiedad de capa).
Patrick Pijnappel
debería ser let innerShadow = CALayer(); innerShadow.frame = bounds. Sin los límites adecuados, no dibujaría la sombra adecuada. Gracias de todos modos
haik.ampardjian
@noir_eagle Es cierto, aunque probablemente quieras configurarlo layoutSubviews()para mantenerlo sincronizado
Patrick Pijnappel
¡Correcto! Ya sea en layoutSubviews()o endraw(_ rect)
haik.ampardjian
24

Un poco indirecto, pero evita tener que usar imágenes (léase: colores fáciles de cambiar, radio de sombra, etc.) y son solo unas pocas líneas de código.

  1. Agregue un UIImageView como la primera subvista de UIView en la que desea la sombra. Yo uso IB, pero puedes hacer lo mismo mediante programación.

  2. Suponiendo que la referencia a UIImageView es 'innerShadow'

'

[[innerShadow layer] setMasksToBounds:YES];
[[innerShadow layer] setCornerRadius:12.0f];        
[[innerShadow layer] setBorderColor:[UIColorFromRGB(180, 180, 180) CGColor]];
[[innerShadow layer] setBorderWidth:1.0f];
[[innerShadow layer] setShadowColor:[UIColorFromRGB(0, 0, 0) CGColor]];
[[innerShadow layer] setShadowOffset:CGSizeMake(0, 0)];
[[innerShadow layer] setShadowOpacity:1];
[[innerShadow layer] setShadowRadius:2.0];

Advertencia: debe tener un borde, o de lo contrario la sombra no aparece. [UIColor clearColor] no funciona. En el ejemplo, uso un color diferente, pero puedes jugar con él para que tenga el mismo color que el comienzo de la sombra. :)

Vea el comentario de bbrame a continuación sobre la UIColorFromRGBmacro.

jinglesthula
fuente
Lo dejé fuera, pero supongo que haría esto como parte de agregar la vista de imagen; asegúrese de configurar el marco en el mismo rect que el UIView principal. Si está utilizando IB, configure los puntales y resortes correctamente para tener el tamaño de la sombra con la vista si va a cambiar el marco de la vista principal. En el código debe haber una máscara de cambio de tamaño que pueda O para hacer lo mismo, AFAIK.
jinglesthula
Esta es la forma más fácil ahora, pero tenga en cuenta que los métodos de sombra CALayer solo están disponibles en iOS 3.2 y posteriores. Soy compatible con 3.1, así que rodeo la configuración de estos atributos en un if ([layer respondsToSelector: @selector (setShadowColor :)]) {
DougW
Esto no parece funcionar para mí. Al menos en xcode 4.2 y ios simulator 4.3. Para que aparezca la sombra, tengo que agregar un color de fondo ... en cuyo punto la sombra aparece solo en el exterior.
Andrea
@Andrea: ten en cuenta la advertencia que mencioné anteriormente. Creo que un color de fondo o un borde pueden tener el mismo efecto de 'darle algo para agregar la sombra'. En cuanto a que aparezca en el exterior, si el UIImageView no es una subvista de la que desea que tenga la sombra interna, podría ser esa, tendría que mirar su código para verlo.
jinglesthula
Solo para rectificar mi declaración anterior ... el código realmente funciona ... Me faltaba algo, pero desafortunadamente no puedo recordarlo en este momento. :) Entonces ... gracias por compartir este fragmento de código.
Andrea
17

Mejor tarde que nunca...

Aquí hay otro enfoque, probablemente no mejor que los que ya se han publicado, pero es agradable y simple:

-(void)drawInnerShadowOnView:(UIView *)view
{
    UIImageView *innerShadowView = [[UIImageView alloc] initWithFrame:view.bounds];

    innerShadowView.contentMode = UIViewContentModeScaleToFill;
    innerShadowView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

    [view addSubview:innerShadowView];

    [innerShadowView.layer setMasksToBounds:YES];

    [innerShadowView.layer setBorderColor:[UIColor lightGrayColor].CGColor];
    [innerShadowView.layer setShadowColor:[UIColor blackColor].CGColor];
    [innerShadowView.layer setBorderWidth:1.0f];

    [innerShadowView.layer setShadowOffset:CGSizeMake(0, 0)];
    [innerShadowView.layer setShadowOpacity:1.0];

    // this is the inner shadow thickness
    [innerShadowView.layer setShadowRadius:1.5];
}
SomaMan
fuente
@SomaMan ¿es posible establecer sombras solo con un lado específico? Como solo en la parte superior o superior / inferior o superior / derecha, etc.
Mitesh Dobareeya
8

En lugar de dibujar una sombra interna con drawRect o agregar UIView a la Vista. Puede Agregar directamente CALayer al borde, por ejemplo: si quiero un efecto de sombra interna en la parte inferior de UIView V.

innerShadowOwnerLayer = [[CALayer alloc]init];
innerShadowOwnerLayer.frame = CGRectMake(0, V.frame.size.height+2, V.frame.size.width, 2);
innerShadowOwnerLayer.backgroundColor = [UIColor whiteColor].CGColor;

innerShadowOwnerLayer.shadowColor = [UIColor blackColor].CGColor;
innerShadowOwnerLayer.shadowOffset = CGSizeMake(0, 0);
innerShadowOwnerLayer.shadowRadius = 10.0;
innerShadowOwnerLayer.shadowOpacity = 0.7;

[V.layer addSubLayer:innerShadowOwnerLayer];

Esto agrega una sombra interior inferior para el objetivo UIView

Jerry Zhang
fuente
6

Aquí hay una versión de swift, change startPointy endPointpara hacerlo en cada lado.

        let layer = CAGradientLayer()
        layer.startPoint    = CGPointMake(0.5, 0.0);
        layer.endPoint      = CGPointMake(0.5, 1.0);
        layer.colors        = [UIColor(white: 0.1, alpha: 1.0).CGColor, UIColor(white: 0.1, alpha: 0.5).CGColor, UIColor.clearColor().CGColor]
        layer.locations     = [0.05, 0.2, 1.0 ]
        layer.frame         = CGRectMake(0, 0, self.view.frame.width, 60)
        self.view.layer.insertSublayer(layer, atIndex: 0)
William Hu
fuente
Trabajó para mi !! Gracias.
iUser
5

Esta es su solución, que he exportado desde PaintCode :

-(void) drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();

    //// Shadow Declarations
    UIColor* shadow = UIColor.whiteColor;
    CGSize shadowOffset = CGSizeMake(0, 0);
    CGFloat shadowBlurRadius = 10;

    //// Rectangle Drawing
    UIBezierPath* rectanglePath = [UIBezierPath bezierPathWithRect: self.bounds];
    [[UIColor blackColor] setFill];
    [rectanglePath fill];

    ////// Rectangle Inner Shadow
    CGContextSaveGState(context);
    UIRectClip(rectanglePath.bounds);
    CGContextSetShadowWithColor(context, CGSizeZero, 0, NULL);

    CGContextSetAlpha(context, CGColorGetAlpha([shadow CGColor]));
    CGContextBeginTransparencyLayer(context, NULL);
    {
        UIColor* opaqueShadow = [shadow colorWithAlphaComponent: 1];
        CGContextSetShadowWithColor(context, shadowOffset, shadowBlurRadius, [opaqueShadow CGColor]);
        CGContextSetBlendMode(context, kCGBlendModeSourceOut);
        CGContextBeginTransparencyLayer(context, NULL);

        [opaqueShadow setFill];
        [rectanglePath fill];

        CGContextEndTransparencyLayer(context);
    }
    CGContextEndTransparencyLayer(context);
    CGContextRestoreGState(context);
}
Dmitriy Kirakosyan
fuente
3

Llego muy tarde a la fiesta, pero me gustaría retribuir a la comunidad ... Este es un método que escribí para eliminar la imagen de fondo de UITextField ya que estaba proporcionando una biblioteca estática y SIN recursos ... Lo usé para una pantalla de entrada de PIN de cuatro instancias de UITextField que podrían mostrar Un carácter sin formato o (BOOL) [self isUsingBullets] o (BOOL) [self usingAsterisks] en ViewController. La aplicación es para iPhone / iPhone retina / iPad / iPad Retina, por lo que no tengo que proporcionar cuatro imágenes ...

#import <QuartzCore/QuartzCore.h>

- (void)setTextFieldInnerGradient:(UITextField *)textField
{

    [textField setSecureTextEntry:self.isUsingBullets];
    [textField setBackgroundColor:[UIColor blackColor]];
    [textField setTextColor:[UIColor blackColor]];
    [textField setBorderStyle:UITextBorderStyleNone];
    [textField setClipsToBounds:YES];

    [textField.layer setBorderColor:[[UIColor blackColor] CGColor]];
    [textField.layer setBorderWidth:1.0f];

    // make a gradient off-white background
    CAGradientLayer *gradient = [CAGradientLayer layer];
    CGRect gradRect = CGRectInset([textField bounds], 3, 3);    // Reduce Width and Height and center layer
    gradRect.size.height += 2;  // minimise Bottom shadow, rely on clipping to remove these 2 pts.

    gradient.frame = gradRect;
    struct CGColor *topColor = [UIColor colorWithWhite:0.6f alpha:1.0f].CGColor;
    struct CGColor *bottomColor = [UIColor colorWithWhite:0.9f alpha:1.0f].CGColor;
    // We need to use this fancy __bridge object in order to get the array we want.
    gradient.colors = [NSArray arrayWithObjects:(__bridge id)topColor, (__bridge id)bottomColor, nil];
    [gradient setCornerRadius:4.0f];
    [gradient setShadowOffset:CGSizeMake(0, 0)];
    [gradient setShadowColor:[[UIColor whiteColor] CGColor]];
    [gradient setShadowOpacity:1.0f];
    [gradient setShadowRadius:3.0f];

    // Now we need to Blur the edges of this layer "so it blends"
    // This rasterizes the view down to 4x4 pixel chunks then scales it back up using bilinear filtering...
    // it's EXTREMELY fast and looks ok if you are just wanting to blur a background view under a modal view.
    // To undo it, just set the rasterization scale back to 1.0 or turn off rasterization.
    [gradient setRasterizationScale:0.25];
    [gradient setShouldRasterize:YES];

    [textField.layer insertSublayer:gradient atIndex:0];

    if (self.usingAsterisks) {
        [textField setFont:[UIFont systemFontOfSize:80.0]];
    } else {
        [textField setFont:[UIFont systemFontOfSize:40.0]];
    }
    [textField setTextAlignment:UITextAlignmentCenter];
    [textField setEnabled:NO];
}

Espero que esto ayude a alguien como este foro me ha ayudado.

Foxnolds
fuente
3

Consulte el gran artículo de Inner Shadows in Quartz de Chris Emery que explica cómo PaintCode dibuja las sombras internas y proporciona un fragmento de código limpio y ordenado:

- (void)drawInnerShadowInContext:(CGContextRef)context
                        withPath:(CGPathRef)path
                     shadowColor:(CGColorRef)shadowColor
                          offset:(CGSize)offset
                      blurRadius:(CGFloat)blurRadius 
{
    CGContextSaveGState(context);

    CGContextAddPath(context, path);
    CGContextClip(context);

    CGColorRef opaqueShadowColor = CGColorCreateCopyWithAlpha(shadowColor, 1.0);

    CGContextSetAlpha(context, CGColorGetAlpha(shadowColor));
    CGContextBeginTransparencyLayer(context, NULL);
        CGContextSetShadowWithColor(context, offset, blurRadius, opaqueShadowColor);
        CGContextSetBlendMode(context, kCGBlendModeSourceOut);
        CGContextSetFillColorWithColor(context, opaqueShadowColor);
        CGContextAddPath(context, path);
        CGContextFillPath(context);
    CGContextEndTransparencyLayer(context);

    CGContextRestoreGState(context);

    CGColorRelease(opaqueShadowColor);
}
voiger
fuente
3

Aquí está mi solución en Swift 4.2. ¿Quieres intentarlo?

final class ACInnerShadowLayer : CAShapeLayer {

  var innerShadowColor: CGColor? = UIColor.black.cgColor {
    didSet { setNeedsDisplay() }
  }

  var innerShadowOffset: CGSize = .zero {
    didSet { setNeedsDisplay() }
  }

  var innerShadowRadius: CGFloat = 8 {
    didSet { setNeedsDisplay() }
  }

  var innerShadowOpacity: Float = 1 {
    didSet { setNeedsDisplay() }
  }

  override init() {
    super.init()

    masksToBounds = true
    contentsScale = UIScreen.main.scale

    setNeedsDisplay()
  }

  override init(layer: Any) {
      if let layer = layer as? InnerShadowLayer {
          innerShadowColor = layer.innerShadowColor
          innerShadowOffset = layer.innerShadowOffset
          innerShadowRadius = layer.innerShadowRadius
          innerShadowOpacity = layer.innerShadowOpacity
      }
      super.init(layer: layer)
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override func draw(in ctx: CGContext) {
    ctx.setAllowsAntialiasing(true)
    ctx.setShouldAntialias(true)
    ctx.interpolationQuality = .high

    let colorspace = CGColorSpaceCreateDeviceRGB()

    var rect = bounds
    var radius = cornerRadius

    if borderWidth != 0 {
      rect = rect.insetBy(dx: borderWidth, dy: borderWidth)
      radius -= borderWidth
      radius = max(radius, 0)
    }

    let innerShadowPath = UIBezierPath(roundedRect: rect, cornerRadius: radius).cgPath
    ctx.addPath(innerShadowPath)
    ctx.clip()

    let shadowPath = CGMutablePath()
    let shadowRect = rect.insetBy(dx: -rect.size.width, dy: -rect.size.width)
    shadowPath.addRect(shadowRect)
    shadowPath.addPath(innerShadowPath)
    shadowPath.closeSubpath()

    if let innerShadowColor = innerShadowColor, let oldComponents = innerShadowColor.components {
      var newComponets = Array<CGFloat>(repeating: 0, count: 4) // [0, 0, 0, 0] as [CGFloat]
      let numberOfComponents = innerShadowColor.numberOfComponents

      switch numberOfComponents {
      case 2:
        newComponets[0] = oldComponents[0]
        newComponets[1] = oldComponents[0]
        newComponets[2] = oldComponents[0]
        newComponets[3] = oldComponents[1] * CGFloat(innerShadowOpacity)
      case 4:
        newComponets[0] = oldComponents[0]
        newComponets[1] = oldComponents[1]
        newComponets[2] = oldComponents[2]
        newComponets[3] = oldComponents[3] * CGFloat(innerShadowOpacity)
      default:
        break
      }

      if let innerShadowColorWithMultipliedAlpha = CGColor(colorSpace: colorspace, components: newComponets) {
        ctx.setFillColor(innerShadowColorWithMultipliedAlpha)
        ctx.setShadow(offset: innerShadowOffset, blur: innerShadowRadius, color: innerShadowColorWithMultipliedAlpha)
        ctx.addPath(shadowPath)
        ctx.fillPath(using: .evenOdd)
      }
    } 
  }
}
Arco
fuente
¿Qué pasa si no lo estoy usando como una clase separada, pero como en mi código, el contexto (ctx) es nulo cuando obtengo esto:let ctx = UIGraphicsGetCurrentContext
Mohsin Khubaib Ahmed
@MohsinKhubaibAhmed Puede obtener el contexto actual por método UIGraphicsGetCurrentContext para buscar cuando algunas vistas empujan su contexto a la pila.
Arco
@Arco Tuve algunos problemas cuando giré el dispositivo. Agregué 'invalidar init de conveniencia (capa: Cualquiera) {self.init ()}'. ¡Ahora no se muestra ningún error!
Yuma Technical Inc.
Se agregó init (capa: Cualquiera) para reparar el bloqueo.
Nik Kov
2

Solución escalable usando CALayer en Swift

Con lo descrito InnerShadowLayertambién puede habilitar sombras internas solo para bordes específicos, excluyendo otros. (por ejemplo, puede habilitar sombras internas solo en los bordes izquierdo y superior de su vista)

Luego puede agregar un InnerShadowLayera su vista usando:

init(...) {

    // ... your initialization code ...

    super.init(frame: .zero)
    layer.addSublayer(shadowLayer)
}

public override func layoutSubviews() {
    super.layoutSubviews()
    shadowLayer.frame = bounds
}

InnerShadowLayer implementación

/// Shadow is a struct defining the different kinds of shadows
public struct Shadow {
    let x: CGFloat
    let y: CGFloat
    let blur: CGFloat
    let opacity: CGFloat
    let color: UIColor
}

/// A layer that applies an inner shadow to the specified edges of either its path or its bounds
public class InnerShadowLayer: CALayer {
    private let shadow: Shadow
    private let edge: UIRectEdge

    public init(shadow: Shadow, edge: UIRectEdge) {
        self.shadow = shadow
        self.edge = edge
        super.init()
        setupShadow()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    public override func layoutSublayers() {
        updateShadow()
    }

    private func setupShadow() {
        shadowColor = shadow.color.cgColor
        shadowOpacity = Float(shadow.opacity)
        shadowRadius = shadow.blur / 2.0
        masksToBounds = true
    }

    private func updateShadow() {
        shadowOffset = {
            let topWidth: CGFloat = 0
            let leftWidth = edge.contains(.left) ? shadow.y / 2 : 0
            let bottomWidth: CGFloat = 0
            let rightWidth = edge.contains(.right) ? -shadow.y / 2 : 0

            let topHeight = edge.contains(.top) ? shadow.y / 2 : 0
            let leftHeight: CGFloat = 0
            let bottomHeight = edge.contains(.bottom) ? -shadow.y / 2 : 0
            let rightHeight: CGFloat = 0

            return CGSize(width: [topWidth, leftWidth, bottomWidth, rightWidth].reduce(0, +),
                          height: [topHeight, leftHeight, bottomHeight, rightHeight].reduce(0, +))
        }()

        let insets = UIEdgeInsets(top: edge.contains(.top) ? -bounds.height : 0,
                                  left: edge.contains(.left) ? -bounds.width : 0,
                                  bottom: edge.contains(.bottom) ? -bounds.height : 0,
                                  right: edge.contains(.right) ? -bounds.width : 0)
        let path = UIBezierPath(rect: bounds.inset(by: insets))
        let cutout = UIBezierPath(rect: bounds).reversing()
        path.append(cutout)
        shadowPath = path.cgPath
    }
}
Pietro Basso
fuente
1

este código funcionó para mí

class InnerDropShadowView: UIView {
    override func draw(_ rect: CGRect) {
        //Drawing code
        let context = UIGraphicsGetCurrentContext()
        //// Shadow Declarations
        let shadow: UIColor? = UIColor.init(hexString: "a3a3a3", alpha: 1.0) //UIColor.black.withAlphaComponent(0.6) //UIColor.init(hexString: "d7d7da", alpha: 1.0)
        let shadowOffset = CGSize(width: 0, height: 0)
        let shadowBlurRadius: CGFloat = 7.5
        //// Rectangle Drawing
        let rectanglePath = UIBezierPath(rect: bounds)
        UIColor.groupTableViewBackground.setFill()
        rectanglePath.fill()
        ////// Rectangle Inner Shadow
        context?.saveGState()
        UIRectClip(rectanglePath.bounds)
        context?.setShadow(offset: CGSize.zero, blur: 0, color: nil)
        context?.setAlpha((shadow?.cgColor.alpha)!)
        context?.beginTransparencyLayer(auxiliaryInfo: nil)
        do {
            let opaqueShadow: UIColor? = shadow?.withAlphaComponent(1)
            context?.setShadow(offset: shadowOffset, blur: shadowBlurRadius, color: opaqueShadow?.cgColor)
            context!.setBlendMode(.sourceOut)
            context?.beginTransparencyLayer(auxiliaryInfo: nil)
            opaqueShadow?.setFill()
            rectanglePath.fill()
            context!.endTransparencyLayer()
        }
        context!.endTransparencyLayer()
        context?.restoreGState()
    }
}
Antony Raphel
fuente
0

Hay un cierto código de aquí que puede hacer esto para usted. Si cambia la capa en su vista (anulando + (Class)layerClass), a JTAInnerShadowLayer, entonces puede configurar la sombra interna en la capa de sangría en su método de inicio y hará el trabajo por usted. Si también desea dibujar el contenido original, asegúrese de llamar setDrawOriginalImage:yesa la capa de sangría. Hay una publicación de blog sobre cómo funciona esto aquí .

James Snook
fuente
@MiteshDobareeya Acabo de probar ambos enlaces y parecen funcionar bien (incluso en una pestaña privada). ¿Qué enlace te estaba causando problemas?
James Snook
¿Puede mirar esta implementación del código de sombra interno? Solo funciona en el método ViewDidAppear. Y muestra algo de parpadeo. drive.google.com/open?id=1VtCt7UFYteq4UteT0RoFRjMfFnbibD0E
Mitesh Dobareeya
0

Usando la capa de degradado:

UIView * mapCover = [UIView new];
mapCover.frame = map.frame;
[view addSubview:mapCover];

CAGradientLayer * vertical = [CAGradientLayer layer];
vertical.frame = mapCover.bounds;
vertical.colors = [NSArray arrayWithObjects:(id)[UIColor whiteColor].CGColor,
                        (id)[[UIColor whiteColor] colorWithAlphaComponent:0.0f].CGColor,
                        (id)[[UIColor whiteColor] colorWithAlphaComponent:0.0f].CGColor,
                        (id)[UIColor whiteColor].CGColor, nil];
vertical.locations = @[@0.01,@0.1,@0.9,@0.99];
[mapCover.layer insertSublayer:vertical atIndex:0];

CAGradientLayer * horizontal = [CAGradientLayer layer];
horizontal.frame = mapCover.bounds;
horizontal.colors = [NSArray arrayWithObjects:(id)[UIColor whiteColor].CGColor,
                     (id)[[UIColor whiteColor] colorWithAlphaComponent:0.0f].CGColor,
                     (id)[[UIColor whiteColor] colorWithAlphaComponent:0.0f].CGColor,
                     (id)[UIColor whiteColor].CGColor, nil];
horizontal.locations = @[@0.01,@0.1,@0.9,@0.99];
horizontal.startPoint = CGPointMake(0.0, 0.5);
horizontal.endPoint = CGPointMake(1.0, 0.5);
[mapCover.layer insertSublayer:horizontal atIndex:0];
Johnny Rockex
fuente
0

Hay una solución simple: simplemente dibuje la sombra normal y gírela, así

@objc func shadowView() -> UIView {
        let shadowView = UIView(frame: .zero)
        shadowView.backgroundColor = .white
        shadowView.layer.shadowColor = UIColor.grey.cgColor
        shadowView.layer.shadowOffset = CGSize(width: 0, height: 2)
        shadowView.layer.shadowOpacity = 1.0
        shadowView.layer.shadowRadius = 4
        shadowView.layer.compositingFilter = "multiplyBlendMode"
        return shadowView
    }

func idtm_addBottomShadow() {
        let shadow = shadowView()
        shadow.transform = transform.rotated(by: 180 * CGFloat(Double.pi))
        shadow.transform = transform.rotated(by: -1 * CGFloat(Double.pi))
        shadow.translatesAutoresizingMaskIntoConstraints = false
        addSubview(shadow)
        NSLayoutConstraint.activate([
            shadow.leadingAnchor.constraint(equalTo: leadingAnchor),
            shadow.trailingAnchor.constraint(equalTo: trailingAnchor),
            shadow.bottomAnchor.constraint(equalTo: bottomAnchor),
            shadow.heightAnchor.constraint(equalToConstant: 1),
            ])
    }
iago849
fuente