Home > database >  Removing Shadow Underneath Semi-Transparent UIView
Removing Shadow Underneath Semi-Transparent UIView

Time:10-31

I’ve been trying to add a drop shadow to a semi transparent UIView but the drop shadow is showing up underneath the view. Basically anywhere inside the outline of the view, I don't want to see any shadows. The location icon has no styling.

Example

// Basic Shadow
self.myView.layer.shadowColor = UIColor.black.cgColor
self.myView.layer.shadowOpacity = 0.3
self.myView.layer.shadowOffset = CGSize(width: 0, height: 3)    
self.myView.layer.shadowRadius = 0

CodePudding user response:

The easiest way to do this is to use a custom UIView subclass with two CAShapeLayers...

For the "shadow" layer path, use a rounded-rect UIBezierPath that is slightly taller than the view, so it extends below the bottom.

Here's a quick example...

Custom View Class

class CustomView: UIView {
    
    public var translucentColor: UIColor = .white.withAlphaComponent(0.7) { didSet { setNeedsLayout() } }
    public var borderColor: UIColor = .init(red: 0.73, green: 0.84, blue: 0.96, alpha: 1.0) { didSet { setNeedsLayout() } }
    public var borderWidth: CGFloat = 4 { didSet { setNeedsLayout() } }
    public var shadowColor: UIColor = .black.withAlphaComponent(0.3) { didSet { setNeedsLayout() } }
    public var cornerRadius: CGFloat = 20 { didSet { setNeedsLayout() } }
    public var offset: CGFloat = 10 { didSet { setNeedsLayout() } }
    
    private let shadowLayer = CAShapeLayer()
    private let topLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    
    private func commonInit() -> Void {
        backgroundColor = .clear
        
        layer.addSublayer(shadowLayer)
        layer.addSublayer(topLayer)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        var r = bounds
        
        // rounded-rect path for visible border
        let pth = UIBezierPath(roundedRect: r, cornerRadius: cornerRadius)
        
        // translucent rounded-rect bordered properties
        topLayer.path = pth.cgPath
        topLayer.fillColor = translucentColor.cgColor
        topLayer.lineWidth = borderWidth
        topLayer.strokeColor = borderColor.cgColor
        
        // rounded-rect path for "shadow" border
        r.size.height  = offset
        let spth = UIBezierPath(roundedRect: r, cornerRadius: cornerRadius)
        
        shadowLayer.path = spth.cgPath
        shadowLayer.fillColor = UIColor.clear.cgColor
        shadowLayer.lineWidth = borderWidth
        shadowLayer.strokeColor = shadowColor.cgColor
    }
    
}

Example Controller Class

class CustomViewTestVC: UIViewController {

    let gradView = BasicGradientView()
    
    let customView = CustomView()
    
    // let's add a label between the gradient view and the custom view
    //  so we can confirm it's translucent
    let testLabel: UILabel = {
        let v = UILabel()
        v.numberOfLines = 0
        v.textAlignment = .center
        v.textColor = .systemBlue
        v.font = .systemFont(ofSize: 34.0, weight: .bold)
        v.text = "This is a test to confirm that the view and the \"shadow\" are both translucent while the border is opaque." // Tap anywhere to toggle this label's visibility."
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBackground
        
        [gradView, testLabel, customView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
        }
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            gradView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            gradView.widthAnchor.constraint(equalToConstant: 312.0),
            gradView.heightAnchor.constraint(equalTo: gradView.widthAnchor, multiplier: 1.0),
            gradView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            
            testLabel.widthAnchor.constraint(equalTo: gradView.widthAnchor, constant: -4.0),
            testLabel.heightAnchor.constraint(equalTo: gradView.heightAnchor, constant: 0.0),
            testLabel.centerXAnchor.constraint(equalTo: gradView.centerXAnchor),
            testLabel.centerYAnchor.constraint(equalTo: gradView.centerYAnchor),

            customView.widthAnchor.constraint(equalTo: gradView.widthAnchor, constant: -90.0),
            customView.heightAnchor.constraint(equalTo: gradView.heightAnchor, constant: -90.0),
            customView.centerXAnchor.constraint(equalTo: gradView.centerXAnchor),
            customView.centerYAnchor.constraint(equalTo: gradView.centerYAnchor),
            
        ])
        
        gradView.endPoint = CGPoint(x: 1.0, y: 1.0)
        
        gradView.colors = [
            .red, .yellow, .cyan,
        ]

    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        testLabel.isHidden.toggle()
    }
    
}

Basic Gradient View

class BasicGradientView: UIView {
    public var colors: [UIColor] = [.white, .black] { didSet { setNeedsLayout() } }
    public var startPoint: CGPoint = CGPoint(x: 0.0, y: 0.0) { didSet { setNeedsLayout() } }
    public var endPoint: CGPoint = CGPoint(x: 1.0, y: 0.0) { didSet { setNeedsLayout() } }

    override class var layerClass: AnyClass {
        return CAGradientLayer.self
    }
    private var gLayer: CAGradientLayer {
        return self.layer as! CAGradientLayer
    }
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }
    func commonInit() {
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        gLayer.colors = colors.compactMap( {$0.cgColor })
        gLayer.startPoint = startPoint
        gLayer.endPoint = endPoint
    }
}

This is the output -- tap anywhere to toggle the UILabel visibility:

enter image description here enter image description here

Then add your imageView on top (or as a subview of the custom view):

enter image description here


Edit - to answer comment

We can get a shadow to show only on the outside by:

  • replacing the "fake-shadow shape layer" with a CALayer
  • using the bezier path as the layer's .shadowPath
  • creating a bezier path with a "hole" cut in it
  • use that path as a CAShapeLayer path
  • and then masking the shadow layer with that CAShapeLayer

Like this:

enter image description here enter image description here

Here are updates to the above code as examples. Both classes are very similar, with the same custom properties that can be changed from their defaults. I've also added a UIImageView as a subview, to produce this output:

enter image description here

as before, tapping anywhere will toggle the UILabel visibility:

enter image description here

CustomViewA Class

class CustomViewA: UIView {
    
    public var translucentColor: UIColor = .white.withAlphaComponent(0.5) { didSet { setNeedsLayout() } }
    public var borderColor: UIColor = .init(red: 0.739, green: 0.828, blue: 0.922, alpha: 1.0) { didSet { setNeedsLayout() } }
    public var borderWidth: CGFloat = 4 { didSet { setNeedsLayout() } }
    public var cornerRadius: CGFloat = 20 { didSet { setNeedsLayout() } }
    
    public var shadowColor: UIColor = .black.withAlphaComponent(0.3) { didSet { setNeedsLayout() } }
    public var shadowOpacity: Float = 0.3
    public var shadowOffset: CGSize = CGSize(width: 0.0, height: 8.0) { didSet { setNeedsLayout() } }
    
    // shadowRadius is not used, but this allows us to treat both CustomViewA and CustomViewB the same
    public var shadowRadius: CGFloat = 0 { didSet { setNeedsLayout() } }

    public var image: UIImage? {
        didSet {
            imageView.image = image
        }
    }
    private let imageView = UIImageView()
    
    private let shadowLayer = CAShapeLayer()
    private let topLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    
    private func commonInit() -> Void {
        backgroundColor = .clear
        
        layer.addSublayer(shadowLayer)
        layer.addSublayer(topLayer)
        
        imageView.contentMode = .scaleAspectFit
        imageView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(imageView)
        
        NSLayoutConstraint.activate([
            imageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.5),
            imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1.0),
            imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
            imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
        ])
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        var r = bounds
        
        // rounded-rect path for visible border
        let pth = UIBezierPath(roundedRect: r, cornerRadius: cornerRadius)
        
        // translucent rounded-rect bordered properties
        topLayer.path = pth.cgPath
        topLayer.fillColor = translucentColor.cgColor
        topLayer.lineWidth = borderWidth
        topLayer.strokeColor = borderColor.cgColor
        
        // rounded-rect path for "shadow" border
        r.size.height  = shadowOffset.height
        let spth = UIBezierPath(roundedRect: r, cornerRadius: cornerRadius)
        
        shadowLayer.path = spth.cgPath
        shadowLayer.fillColor = UIColor.clear.cgColor
        shadowLayer.lineWidth = borderWidth
        shadowLayer.strokeColor = shadowColor.cgColor
    }
    
}

CustomViewB Class

class CustomViewB: UIView {
    
    public var translucentColor: UIColor = .white.withAlphaComponent(0.5) { didSet { setNeedsLayout() } }
    public var borderColor: UIColor = .init(red: 0.739, green: 0.828, blue: 0.922, alpha: 1.0) { didSet { setNeedsLayout() } }
    public var borderWidth: CGFloat = 4 { didSet { setNeedsLayout() } }
    public var cornerRadius: CGFloat = 20 { didSet { setNeedsLayout() } }
    
    public var shadowColor: UIColor = .black { didSet { setNeedsLayout() } }
    public var shadowOpacity: Float = 0.7
    public var shadowOffset: CGSize = CGSize(width: 0.0, height: 10.0) { didSet { setNeedsLayout() } }
    public var shadowRadius: CGFloat = 6 { didSet { setNeedsLayout() } }
    
    public var image: UIImage? {
        didSet {
            imageView.image = image
        }
    }
    private let imageView = UIImageView()
    
    private let shadowLayer = CALayer()
    private let topLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    
    private func commonInit() -> Void {
        backgroundColor = .clear
        
        layer.addSublayer(shadowLayer)
        layer.addSublayer(topLayer)
        
        // add a square (1:1) image view, 1/2 the width of self
        //  centered horizontally and vertically
        imageView.contentMode = .scaleAspectFit
        imageView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(imageView)
        
        NSLayoutConstraint.activate([
            imageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.5),
            imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1.0),
            imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
            imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
        ])
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        // rounded-rect path for visible border
        let pth = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
        
        // translucent rounded-rect bordered properties
        topLayer.path = pth.cgPath
        topLayer.fillColor = translucentColor.cgColor
        topLayer.lineWidth = borderWidth
        topLayer.strokeColor = borderColor.cgColor
        
        // we're going to mask the shadow layer with a "cutout" of the rounded rect
        //  the shadow is going to spread outside the bounds,
        //  so the "outer" path needs to be larger
        //  we'll make it plenty large enough
        let bpth = UIBezierPath(rect: bounds.insetBy(dx: -bounds.width, dy: -bounds.height))
        bpth.append(pth)
        bpth.usesEvenOddFillRule = true
        
        let maskLayer = CAShapeLayer()
        maskLayer.fillRule = .evenOdd
        maskLayer.path = bpth.cgPath
        shadowLayer.mask = maskLayer
        
        shadowLayer.shadowPath = pth.cgPath
        shadowLayer.shadowOpacity = shadowOpacity
        shadowLayer.shadowColor = shadowColor.cgColor
        shadowLayer.shadowRadius = shadowRadius
        shadowLayer.shadowOffset = shadowOffset
    }
    
}

Example Controller Class - uses the BasicGradientView class above

class CustomViewTestVC: UIViewController {

    let gradViewA = BasicGradientView()
    let gradViewB = BasicGradientView()

    let customViewA = CustomViewA()
    let customViewB = CustomViewB()

    // let's add a label between the gradient view and the custom view
    //  so we can confirm it's translucent
    let testLabelA: UILabel = {
        let v = UILabel()
        v.numberOfLines = 0
        v.textAlignment = .center
        v.textColor = .systemRed
        v.font = .systemFont(ofSize: 32.0, weight: .regular)
        return v
    }()

    let testLabelB: UILabel = {
        let v = UILabel()
        return v
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBackground
        
        [gradViewA, gradViewB, testLabelA, testLabelB, customViewA, customViewB].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
        }
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            gradViewA.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
            gradViewA.widthAnchor.constraint(equalToConstant: 300.0),
            gradViewA.heightAnchor.constraint(equalTo: gradViewA.widthAnchor, multiplier: 1.0),
            gradViewA.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            
            testLabelA.widthAnchor.constraint(equalTo: gradViewA.widthAnchor, constant: -4.0),
            testLabelA.heightAnchor.constraint(equalTo: gradViewA.heightAnchor, constant: 0.0),
            testLabelA.centerXAnchor.constraint(equalTo: gradViewA.centerXAnchor),
            testLabelA.centerYAnchor.constraint(equalTo: gradViewA.centerYAnchor),

            customViewA.widthAnchor.constraint(equalTo: gradViewA.widthAnchor, constant: -84.0),
            customViewA.heightAnchor.constraint(equalTo: gradViewA.heightAnchor, constant: -84.0),
            customViewA.centerXAnchor.constraint(equalTo: gradViewA.centerXAnchor),
            customViewA.centerYAnchor.constraint(equalTo: gradViewA.centerYAnchor),
            
            gradViewB.topAnchor.constraint(equalTo: gradViewA.bottomAnchor, constant: 8.0),
            gradViewB.widthAnchor.constraint(equalTo: gradViewA.widthAnchor, constant: 0.0),
            gradViewB.heightAnchor.constraint(equalTo: gradViewB.widthAnchor, multiplier: 1.0),
            gradViewB.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            
            testLabelB.widthAnchor.constraint(equalTo: testLabelA.widthAnchor, constant: -0.0),
            testLabelB.heightAnchor.constraint(equalTo: testLabelA.heightAnchor, constant: 0.0),
            testLabelB.centerXAnchor.constraint(equalTo: gradViewB.centerXAnchor),
            testLabelB.centerYAnchor.constraint(equalTo: gradViewB.centerYAnchor),
            
            customViewB.widthAnchor.constraint(equalTo: customViewA.widthAnchor, constant: 0.0),
            customViewB.heightAnchor.constraint(equalTo: customViewA.heightAnchor, constant: 0.0),
            customViewB.centerXAnchor.constraint(equalTo: gradViewB.centerXAnchor),
            customViewB.centerYAnchor.constraint(equalTo: gradViewB.centerYAnchor),
            
        ])
        
        // let's setup the gradient views the same
        gradViewA.colors = [
            .init(red: 0.242, green: 0.591, blue: 0.959, alpha: 1.0),
            .init(red: 0.113, green: 0.472, blue: 0.866, alpha: 1.0)
        ]
        gradViewA.endPoint = CGPoint(x: 1.0, y: 1.0)

        gradViewB.colors = gradViewA.colors
        gradViewB.endPoint = gradViewA.endPoint

        // let's give the two test labels the same properties
        testLabelB.numberOfLines = testLabelA.numberOfLines
        testLabelB.textAlignment = testLabelA.textAlignment
        testLabelB.textColor = testLabelA.textColor
        testLabelB.font = testLabelA.font
        
        let s = "This is a test to confirm that the view and the \"shadow\" are both translucent while the border is opaque."
        testLabelA.text = "CustomViewA\n"   s
        testLabelB.text = "CustomViewB\n"   s

        // set the .image property of both custom views
        if let img = UIImage(named: "marker") {
            customViewA.image = img
            customViewB.image = img
        } else {
            if let img = UIImage(systemName: "mappin.and.ellipse")?.withTintColor(.white, renderingMode: .alwaysOriginal) {
                customViewA.image = img
                customViewB.image = img
            }
        }

    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        testLabelA.isHidden.toggle()
        testLabelB.isHidden.toggle()
    }
    
}
  • Related