Search code examples
swiftuiimageview

Swift - drop shadow on the masked image


I'm just trying to add a shadow to the image, which is not just rectangular, I'd like to apply the same mask effect as image has. On stack overflow I've found the same question, but the answer is suggested on objective-c language - iOS SDK - drop shadow on masked image Can you please help me wit swift-code on this question. Thanks in advance.

class drawView: UIView {
override func draw(_ rect: CGRect) {
    super.draw(rect)
    guard let context = UIGraphicsGetCurrentContext() else {
        return
    }
    self.drawCircle(context)
}
private func drawCircle(_ context: CGContext){
    context.setStrokeColor(UIColor.black.cgColor)
    let newMask = CAShapeLayer()
    let circlePath = UIBezierPath(ovalIn: .init(x: 0, y: 0, width: 50, height: 50))
    circlePath.stroke()
    newMask.path = circlePath.cgPath
    self.layer.mask = newMask //if I apply this, then shadow disappears
    
    self.layer.shadowColor = UIColor.black.cgColor
    self.layer.shadowOpacity = 0.15
    self.layer.shadowOffset = .init(width: 10, height: -10)
    self.layer.shadowRadius = 2
}

}


Solution

  • There are various ways to approach this.

    I would suggest using a custom UIView with a UIImageView subview:

    We mask the imageView, and then apply the layer shadow to the custom view itself:

    class ShadowMaskImageView: UIView {
        
        let theImageView = UIImageView()
        let maskShape = CAShapeLayer()
    
        var image: UIImage = UIImage() {
            didSet {
                theImageView.image = image
            }
        }
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() -> Void {
            theImageView.translatesAutoresizingMaskIntoConstraints = false
            addSubview(theImageView)
            NSLayoutConstraint.activate([
                theImageView.topAnchor.constraint(equalTo: topAnchor),
                theImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
                theImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
                theImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
            ])
    
            theImageView.layer.mask = maskShape
    
            layer.shadowOffset = CGSize(width: 10.0, height: -10.0)
            layer.shadowColor = UIColor.black.cgColor
            layer.shadowRadius = 2
            layer.shadowOpacity = 0.15
            
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
    
            maskShape.frame = bounds
    
            // let's inset the image frame by 10-pts on all sides
            //  so our 10 x -10 shadow will fit inside the bounds
            let r = bounds.insetBy(dx: 10.0, dy: 10.0)
            
            // oval path
            let pth = UIBezierPath(ovalIn: r)
            maskShape.path = pth.cgPath
            
        }
        
    }
    

    Here's how it looks (red outline rectangle is just showing the view frame)...

    First, without the mask OR shadow:

    enter image description here

    Now, with just the mask:

    enter image description here

    and, with the mask AND shadow:

    enter image description here

    and finally, without the red border showing the frame:

    enter image description here

    Here's an example view controller:

    class MaskShadowViewController: UIViewController {
        
        let testView = ShadowMaskImageView()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            // make sure we can load the image
            guard let img = UIImage(named: "sample") else {
                fatalError("Could not load sample image!!!")
            }
            
            // set the image
            testView.image = img
    
            // add testView and set its height equal to the ratio of the original image
            testView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(testView)
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                testView.heightAnchor.constraint(equalTo: testView.widthAnchor, multiplier: img.size.height / img.size.width),
                testView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            ])
    
            // un-comment the following if we want to see the frame of our custom view
    
            //let frameView = UIView()
            //frameView.translatesAutoresizingMaskIntoConstraints = false
            //view.addSubview(frameView)
            //NSLayoutConstraint.activate([
            //  frameView.topAnchor.constraint(equalTo: testView.topAnchor, constant: 0.0),
            //  frameView.leadingAnchor.constraint(equalTo: testView.leadingAnchor, constant: 0.0),
            //  frameView.trailingAnchor.constraint(equalTo: testView.trailingAnchor, constant: 0.0),
            //  frameView.bottomAnchor.constraint(equalTo: testView.bottomAnchor, constant: 0.0),
            //])
            //
            //frameView.layer.borderWidth = 1
            //frameView.layer.borderColor = UIColor.red.cgColor
    
        }
        
    }