Efeito de sombra interna na camada UIView?

92

Eu tenho o seguinte 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];

Eu gostaria de adicionar um efeito de sombra interna a ele, mas não tenho certeza de como fazer isso. Suponho que seria necessário desenhar em drawRect, no entanto, isso adicionaria a camada em cima de outros objetos UIView, uma vez que deveria ser uma barra atrás de alguns botões, então não sei o que fazer?

Eu poderia adicionar outra camada, mas, novamente, não tenho certeza de como conseguir o efeito de sombra interna (como este:

insira a descrição da imagem aqui

Ajuda apreciada ...

runmad
fonte

Respostas:

108

Para qualquer pessoa que esteja se perguntando como desenhar uma sombra interna usando Core Graphics de acordo com a sugestão de Costique, faça o seguinte: (no iOS ajuste conforme necessário)

Em seu 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);

Então, essencialmente, existem as seguintes etapas:

  1. Crie seu caminho
  2. Defina a cor de preenchimento desejada, adicione este caminho ao contexto e preencha o contexto
  3. Agora crie um retângulo maior que pode limitar o caminho visível. Antes de fechar este caminho, adicione o caminho visível. Em seguida, feche o caminho para criar uma forma com o caminho visível subtraído dele. Você pode querer investigar os métodos de preenchimento (enrolamento diferente de zero de par / ímpar) dependendo de como você criou esses caminhos. Em essência, para fazer com que os subcaminhos "subtraiam" quando você os soma, você precisa desenhá-los (ou melhor, construí-los) em direções opostas, um no sentido horário e outro no sentido anti-horário.
  4. Em seguida, você precisa definir seu caminho visível como o caminho de recorte no contexto, para que você não desenhe nada fora dele na tela.
  5. Em seguida, configure a sombra no contexto, que inclui o deslocamento, desfoque e cor.
  6. Em seguida, preencha a forma grande com o orifício. A cor não importa, porque se você fez tudo certo, você não verá esta cor, apenas a sombra.
Daniel Thorpe
fonte
Obrigado, mas é possível ajustar o raio? Atualmente é baseado nos limites, mas eu gostaria de basear em um raio definido (como 5.0f). Com o código acima, é arredondado demais.
runmad de
2
@runmad Bem, você pode criar qualquer tipo de CGPath visível que desejar, o exemplo usado aqui é apenas esse, um exemplo, escolhido para abreviar. Se você gostaria de criar um retângulo arredondado, você pode simplesmente fazer algo como: CGPath visiblePath = [UIBezierPath bezierPathWithRoundedRect: rect cornerRadius: radius] .CGPath Espero que ajude.
Daniel Thorpe,
4
@DanielThorpe: +1 pela boa resposta. Corrigi o path code retângulo arredondado (o seu quebrou ao alterar o raio) e simplifiquei o path code retângulo externo. Espero que você não se importe.
Regexident
Como posso configurar corretamente a sombra interna em 4 direções, não apenas em 2?
Protocole
@Protocole você pode definir o deslocamento para {0,0}, mas use um raio de sombra de, digamos, 4.f.
Daniel Thorpe
47

Sei que estou atrasado para essa festa, mas isso teria me ajudado a descobrir no início de minhas viagens ...

Para dar crédito a quem o crédito é devido, esta é essencialmente uma modificação da elaboração de Daniel Thorpe sobre a solução de Costique de subtrair uma região menor de uma região maior. Esta versão é para quem usa composição de camada em vez de substituir-drawRect:

A CAShapeLayerclasse pode ser usada para obter o mesmo efeito:

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];

Nesse ponto, se sua camada pai não estiver mascarando até os limites, você verá a área extra da camada de máscara ao redor das bordas da camada. Isso terá 42 pixels de preto se você apenas copiou o exemplo diretamente. Para se livrar dele, você pode simplesmente usar outro CAShapeLayercom o mesmo caminho e defini-lo como a máscara da camada de sombra:

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

Eu mesmo não fiz o benchmarking, mas suspeito que usar essa abordagem em conjunto com a rasterização tem mais desempenho do que substituição -drawRect:.

Matt Wilding
fonte
3
someInnerPath? Você poderia explicar um pouco mais, por favor.
Moe
4
@Moe Pode ser qualquer CGPath arbitrário que você quiser. [[UIBezierPath pathWithRect:[shadowLayer bounds]] CGPath]sendo a escolha mais simples.
Matt Wilding
Um abraço Matt :-)
Moe
Estou obtendo um retângulo preto (externo) para shadowLayer.path que desenha corretamente a sombra interna. Como posso me livrar dele (o retângulo externo preto)? Parece que você só pode definir o fillColor dentro de um contexto e você não usa um.
Olivier de
11
Isso funciona muito bem! Eu carreguei no github com algumas adições. Experimente :) github.com/inamiy/YIInnerShadowView
inamiy
35

É possível desenhar uma sombra interna com Core Graphics criando um grande caminho retângulo fora dos limites, subtraindo um caminho retângulo do tamanho dos limites e preenchendo o caminho resultante com uma sombra "normal".

No entanto, como você precisa combiná-lo com uma camada de gradiente, acho que uma solução mais fácil é criar uma imagem PNG transparente de 9 partes da sombra interna e esticá-la para o tamanho certo. A imagem sombreada em 9 partes ficaria assim (seu tamanho é 21x21 pixels):

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);

Em seguida, defina a moldura de innerShadowLayer e ela deve esticar a sombra corretamente.

Costique
fonte
Sim, suponho que você esteja certo. Só queria que a camada fosse o mais plana possível. Eu poderia criar a imagem no Photoshop com a sombra interna e aparência de gradiente, só tenho problemas com as cores combinando até 100% no dispositivo ao usar uma imagem.
runmad
Sim, isso é um problema com todos os gradientes e sombras, simplesmente não consigo reproduzir esses efeitos do Photoshop 1: 1 no iOS, por mais que tente.
Costique
29

Uma versão simplificada usando apenas CALayer, em 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)
    }
}

Observe que a camada de sombra interna não deve ter uma cor de fundo opaca, pois isso será renderizado na frente da sombra.

Patrick Pijnappel
fonte
A última linha contém 'camada'. De onde vem isto?
Charlie Seligman
@CharlieSeligman É a camada pai, que pode ser qualquer camada. Você pode usar uma camada personalizada ou a camada da visualização (UIView tem uma propriedade de camada).
Patrick Pijnappel
deveria ser let innerShadow = CALayer(); innerShadow.frame = bounds. Sem os limites adequados, não atrairia a sombra adequada. Obrigado de qualquer maneira
haik.ampardjian
@noir_eagle Verdadeiro, embora você provavelmente queira configurá-lo layoutSubviews()para mantê-lo sincronizado
Patrick Pijnappel
Certo! Em layoutSubviews()ou emdraw(_ rect)
haik.ampardjian
24

É um pouco circular, mas evita ter que usar imagens (leia-se: fácil mudar cores, raio de sombra, etc.) e são apenas algumas linhas de código.

  1. Adicione um UIImageView como a primeira subvisualização do UIView no qual deseja colocar a sombra. Eu uso IB, mas você pode fazer o mesmo programaticamente.

  2. Supondo que a referência ao UIImageView seja '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];

Advertência: você precisa ter uma borda, ou então a sombra não aparecerá. [UIColor clearColor] não funciona. No exemplo, eu uso uma cor diferente, mas você pode mexer com ela para que fique com a mesma cor do início da sombra. :)

Veja o comentário do bbrame abaixo sobre a UIColorFromRGBmacro.

Jinglesthula
fonte
Eu o deixei de fora, mas suponha que você faria isso como parte da adição da visualização da imagem - certifique-se de definir o quadro com o mesmo retângulo do UIView pai. Se você estiver usando IB, defina as escoras e molas corretas para ter o tamanho da sombra com a vista, se for alterar o quadro da vista pai. No código deve haver uma máscara de redimensionamento que você pode OU para fazer o mesmo, AFAIK.
jinglesthula
Esta é a maneira mais fácil agora, mas esteja ciente de que os métodos de sombra CALayer estão disponíveis apenas no iOS 3.2 e posterior. Eu suporte 3.1, então eu coloco esses atributos em um if ([layer respondsToSelector: @selector (setShadowColor :)]) {
DougW
Isso não parece funcionar para mim. Pelo menos no xcode 4.2 e simulador ios 4.3. Para fazer a sombra aparecer, tenho que adicionar uma cor de fundo ... nesse ponto, a sombra aparece apenas do lado de fora.
Andrea
@Andrea - tenha em mente a advertência que mencionei acima. Acho que uma cor de fundo ou uma borda pode ter o mesmo efeito de 'dar algo para adicionar a sombra'. Quanto a aparecer do lado de fora, se UIImageView não for uma subvisualização daquela em que você deseja que a sombra interna seja, eu teria que olhar seu código para ver.
jinglesthula
Só para retificar minha declaração anterior ... o código realmente funciona ... Eu estava faltando alguma coisa, mas infelizmente não consigo me lembrar agora. :) Então ... obrigado por compartilhar este trecho de código.
Andrea
17

Antes tarde do que nunca...

Aqui está outra abordagem, provavelmente não melhor do que as já postadas, mas é agradável e simples -

-(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
fonte
@SomaMan é possível definir a sombra apenas com o lado específico? Como apenas na parte superior ou superior / inferior ou superior / direita etc.
Mitesh Dobareeya
8

Em vez de desenhar a sombra interna por drawRect ou adicionar UIView à vista. Você pode adicionar CALayer diretamente à borda, por exemplo: se eu quiser efeito de sombra interna na parte inferior do 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];

Isso adiciona uma sombra interna inferior para o UIView de destino

Jerry Zhang
fonte
6

Aqui está uma versão do swift, change startPointe endPointto make it em 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
fonte
Funcionou para mim !! Obrigado.
iUser
5

Esta é a sua solução, que exportei do 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
fonte
3

Estou muito atrasado para a festa, mas gostaria de retribuir à comunidade .. Este é um método que escrevi para remover a imagem de fundo UITextField porque estava fornecendo uma biblioteca estática e NENHUM recurso ... Usei isso para uma tela de entrada de PIN de quatro instâncias de UITextField que podem exibir Um caractere bruto ou (BOOL) [self isUsingBullets] ou (BOOL) [self usingAsterisks] em ViewController. O aplicativo é para iPhone / iPhone retina / iPad / iPad Retina, então não preciso fornecer quatro imagens ...

#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 isso ajude alguém, pois este fórum me ajudou.

Foxnolds
fonte
3

Confira o ótimo artigo de Inner Shadows in Quartz de Chris Emery que explica como as sombras internas são desenhadas pelo PaintCode e fornece um trecho de código limpo e organizado:

- (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
fonte
3

Aqui está minha solução no Swift 4.2. Você gostaria de tentar?

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
fonte
E se eu não estiver usando como uma classe separada, mas como usando no meu código, o contexto (ctx) é nulo quando eu obtenho isto:let ctx = UIGraphicsGetCurrentContext
Mohsin Khubaib Ahmed
@MohsinKhubaibAhmed Você pode obter o contexto atual por método UIGraphicsGetCurrentContext para buscar quando algumas visualizações colocam seu contexto na pilha.
Arco
@Arco Tive alguns problemas quando girei o dispositivo. Eu adicionei 'override convenient init (layer: Any) {self.init ()}'. Agora nenhum erro é exibido!
Yuma Technical Inc.
Adicionado init (camada: Qualquer) para corrigir o travamento.
Nik Kov
2

Solução escalável usando CALayer em Swift

Com o descrito InnerShadowLayer você também pode ativar sombras internas apenas para bordas específicas, excluindo outras. (por exemplo, você pode ativar sombras internas apenas nas bordas esquerda e superior de sua visualização)

Você pode então adicionar um InnerShadowLayer à sua visualização usando:

init(...) {

    // ... your initialization code ...

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

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

InnerShadowLayer implementação

/// 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
fonte
1

este código funcionou para mim

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
fonte
0

Há algum código aqui que pode fazer isso por você. Se você alterar a camada em sua visualização (substituindo + (Class)layerClass), para JTAInnerShadowLayer, então você pode definir a sombra interna na camada de indentação em seu método init e ele fará o trabalho para você. Se você também deseja desenhar o conteúdo original, certifique-se de chamar setDrawOriginalImage:yesa camada de recuo. Há uma postagem no blog sobre como isso funciona aqui .

James Snook
fonte
@MiteshDobareeya Acabei de testar os dois links e eles parecem funcionar bem (inclusive em uma guia privada). Qual link estava causando problemas?
James Snook
Você pode, por favor, dar uma olhada nesta implementação do código de sombra interna. Funciona apenas no método ViewDidAppear. E mostra alguma cintilação. drive.google.com/open?id=1VtCt7UFYteq4UteT0RoFRjMfFnbibD0E
Mitesh Dobareeya
0

Usando a camada de gradiente:

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
fonte
0

Existe uma solução simples - basta desenhar a sombra normal e girar, assim

@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
fonte