Qual é a melhor maneira de adicionar uma sombra projetada ao meu UIView

108

Estou tentando adicionar uma sombra projetada para visualizações que são colocadas em camadas umas sobre as outras, as visualizações colapsam permitindo que o conteúdo em outras visualizações seja visto, neste sentido, eu quero manter view.clipsToBoundsON para que, quando as visualizações colapsarem, seu conteúdo seja cortado.

Isso parece ter dificultado a adição de uma sombra projetada às camadas, pois quando eu ligo clipsToBoundsas sombras também são cortadas.

Tenho tentado manipular view.framee view.boundsadicionar uma sombra projetada ao quadro, mas permitir que os limites sejam grandes o suficiente para englobá-lo, no entanto, não tive sorte com isso.

Aqui está o código que estou usando para adicionar uma sombra (isso só funciona com clipsToBoundsOFF, conforme mostrado)

view.clipsToBounds = NO;
view.layer.shadowColor = [[UIColor blackColor] CGColor];
view.layer.shadowOffset = CGSizeMake(0,5);
view.layer.shadowOpacity = 0.5;

Aqui está uma captura de tela da sombra sendo aplicada à camada superior cinza mais claro. Espero que isso dê uma ideia de como meu conteúdo se sobreporá se clipsToBoundsestiver DESLIGADO.

Aplicação de sombra.

Como posso adicionar uma sombra UIViewe manter meu conteúdo cortado?

Edit: Só queria acrescentar que também brinquei com o uso de imagens de fundo com sombras, o que funciona bem, no entanto, ainda gostaria de saber a melhor solução codificada para isso.

Wez
fonte

Respostas:

280

Experimente isto:

UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRect:view.bounds];
view.layer.masksToBounds = NO;
view.layer.shadowColor = [UIColor blackColor].CGColor;
view.layer.shadowOffset = CGSizeMake(0.0f, 5.0f);
view.layer.shadowOpacity = 0.5f;
view.layer.shadowPath = shadowPath.CGPath;

Em primeiro lugar : o UIBezierPathusado como shadowPathé crucial. Se você não o usar, pode não notar a diferença no início, mas o olho atento observará um certo atraso ocorrendo durante eventos como girar o dispositivo e / ou similar. É um ajuste de desempenho importante.

Especificamente em relação ao seu problema : A linha importante é view.layer.masksToBounds = NO. Ele desativa o recorte das subcamadas da camada de visualização que se estendem além dos limites da visualização.

Para aqueles que estão se perguntando qual é a diferença entre masksToBounds(na camada) e a clipToBoundspropriedade da própria visualização : Não existe nenhuma. Alternar um terá efeito sobre o outro. Apenas um nível diferente de abstração.


Swift 2.2:

override func layoutSubviews()
{
    super.layoutSubviews()

    let shadowPath = UIBezierPath(rect: bounds)
    layer.masksToBounds = false
    layer.shadowColor = UIColor.blackColor().CGColor
    layer.shadowOffset = CGSizeMake(0.0, 5.0)
    layer.shadowOpacity = 0.5
    layer.shadowPath = shadowPath.CGPath
}

Swift 3:

override func layoutSubviews()
{
    super.layoutSubviews()

    let shadowPath = UIBezierPath(rect: bounds)
    layer.masksToBounds = false
    layer.shadowColor = UIColor.black.cgColor
    layer.shadowOffset = CGSize(width: 0.0, height: 5.0)
    layer.shadowOpacity = 0.5
    layer.shadowPath = shadowPath.cgPath
}
pkluz
fonte
1
Obrigado, tentei seu código e depois tentei adicionar masksToBounds = NO;ao meu original - com ambas as tentativas eu mantive clipsToBounds = YES;ON - ambos falharam em cortar o conteúdo. aqui está uma captura de tela
Wez
1
Alguma ideia de como fazer com que a sombra projetada abrace um canto arredondado em vez de renderizar como se o canto ainda fosse um quadrado?
John Erck
7
@JohnErck tente o seguinte: UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds cornerRadius:5.0];. Não testado, mas deve produzir o resultado desejado.
pkluz
1
Aprendi que, ao animar a vista, a sombra deixa rastros cinzentos enquanto se move. Remover as linhas do shadowPath resolveu isso
ishahak
1
@shoe porque qualquer hora que o tamanho da vista muda, layoutSubviews()é chamado. E se não compensarmos nesse local ajustando o shadowPathpara refletir o tamanho atual, teríamos uma visão redimensionada, mas a sombra ainda teria o tamanho antigo (inicial) e não mostraria ou espiaria onde não deveria .
pkluz
65

A resposta de Wasabii no Swift 2.3:

let shadowPath = UIBezierPath(rect: view.bounds)
view.layer.masksToBounds = false
view.layer.shadowColor = UIColor.blackColor().CGColor
view.layer.shadowOffset = CGSize(width: 0, height: 0.5)
view.layer.shadowOpacity = 0.2
view.layer.shadowPath = shadowPath.CGPath

E em Swift 3/4/5:

let shadowPath = UIBezierPath(rect: view.bounds)
view.layer.masksToBounds = false
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOffset = CGSize(width: 0, height: 0.5)
view.layer.shadowOpacity = 0.2
view.layer.shadowPath = shadowPath.cgPath

Coloque este código em layoutSubviews () se você estiver usando AutoLayout.

No SwiftUI, isso é muito mais fácil:

Color.yellow  // or whatever your view
    .shadow(radius: 3)
    .frame(width: 200, height: 100)
Bart van Kuik
fonte
11
Obrigado por observarlayoutSubviews
Islam Q.
1
ou ainda melhor em viewDidLayoutSubviews ()
Max MacLeod
1
preferem initialisers rápida struct sobre Marca variantes, ou sejaCGSize(width: 0, height: 0.5)
Oliver Atkinson
Eu adicionei este código a layoutSubviews (), ele ainda não foi renderizado em IB. Existem outras configurações que eu devo ativar?
burro
Obrigado. Eu estava usando o Auto Layout e pensei comigo mesmo - bem ... view.boundsnão foi criado de forma tão óbvia que shadowPathnão pode fazer referência ao retângulo da visualização, mas sim a alguns CGRectZero. Enfim, colocar isso layoutSubviewsresolveu meu problema finalmente!
devdc
13

O truque é definir a masksToBoundspropriedade da camada de sua visualização corretamente:

view.layer.masksToBounds = NO;

e deve funcionar.

( Fonte )

Sergio
fonte
13

Você pode criar uma extensão para UIView para acessar esses valores no editor de design

Opções de sombra no editor de design

extension UIView{

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

    @IBInspectable var shadowColor: UIColor{
        get{
            return UIColor(cgColor: self.layer.shadowColor!)
        }
        set{
            self.layer.shadowColor = newValue.cgColor
        }
    }

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

    @IBInspectable var shadowOpacity: Float{
        get{
            return self.layer.shadowOpacity
        }
        set{
            self.layer.shadowOpacity = newValue
        }
    }
}
Chris Stillwell
fonte
como se referir a essa extensão no construtor de interface
Amr Angry
IBInspectable o torna disponível no construtor de interface
Nitesh
Posso conseguir isso no Objective C também?
Harish Pathak
2
se você quiser ter uma visualização em tempo real (no construtor de interface), aqui você pode encontrar um bom artigo sobre isso spin.atomicobject.com/2017/07/18/swift-interface-builder
medskill
7

Você pode definir a sombra para a sua visão do storyboard também

insira a descrição da imagem aqui

Pallavi
fonte
2

Em viewWillLayoutSubviews:

override func viewWillLayoutSubviews() {
    sampleView.layer.masksToBounds =  false
    sampleView.layer.shadowColor = UIColor.darkGrayColor().CGColor;
    sampleView.layer.shadowOffset = CGSizeMake(2.0, 2.0)
    sampleView.layer.shadowOpacity = 1.0
}

Usando a extensão do UIView:

extension UIView {

    func addDropShadowToView(targetView:UIView? ){
        targetView!.layer.masksToBounds =  false
        targetView!.layer.shadowColor = UIColor.darkGrayColor().CGColor;
        targetView!.layer.shadowOffset = CGSizeMake(2.0, 2.0)
        targetView!.layer.shadowOpacity = 1.0
    }
}

Uso:

sampleView.addDropShadowToView(sampleView)
AG
fonte
1
Apenas aplique targetView: UIViewe remova a franja.
bluebamboo
6
Por que não usar self? Parece muito mais conveniente para mim.
LinusGeffarth
3
A melhor maneira de fazer isso é remover targetView como um parâmetro e usar self em vez de targetView. Isso elimina a redundância e permite chamá-lo assim: sampleView.addDropShadowToView ()
maxcodes
Os comentários de Max Nelson são ótimos. Minha sugestão é usar if letsintaxe em vez de!
Johnny
0

Portanto, sim, você deve preferir a propriedade shadowPath para desempenho, mas também: Do arquivo de cabeçalho de CALayer.shadowPath

Especificar o caminho explicitamente usando essa propriedade geralmente * melhorará o desempenho de renderização, assim como compartilhará o mesmo caminho * de referência em várias camadas

Um truque menos conhecido é compartilhar a mesma referência em várias camadas. Claro que eles têm que usar a mesma forma, mas isso é comum com células de visualização de tabela / coleção.

Não sei por que fica mais rápido se você compartilhar instâncias, acho que ele armazena em cache a renderização da sombra e pode reutilizá-la para outras instâncias no modo de exibição. Eu me pergunto se isso é ainda mais rápido com

Rufus Mall
fonte