skip to Main Content

I am trying to get erase functionality working with CAShapeLayer. Current code:

class MaskTestVC: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .white
        let image = UIImage(named: "test.jpg")!

        let testLayer = CAShapeLayer()
        testLayer.contents = image.cgImage
        testLayer.frame = view.bounds

        let maskLayer = CAShapeLayer()
        maskLayer.opacity = 1.0
        maskLayer.lineWidth = 20.0
        maskLayer.strokeColor = UIColor.black.cgColor
        maskLayer.fillColor = UIColor.clear.cgColor
        maskLayer.frame = view.bounds
        testLayer.mask = maskLayer

        view.layer.addSublayer(testLayer)
    }

}

I was thinking if I create a mask layer then I could draw a path on the mask layer and erase parts of the image eg:

let path = UIBezierPath()
path.move(to: CGPoint(x: 100.0, y: 100.0))
path.addLine(to: CGPoint(x: 200.0, y: 200.0))
maskLayer.path = path.cgPath

However it seems that when I add the maskLayer it covers the entire image and I can’t see the contents below it. What am I doing wrong here?

2

Answers


  1. maskLayer has no initial path therefore its content is not filled, also filling it with .clear will completely mask the image.

    This works for me:

    override func viewDidLoad() {
       super.viewDidLoad()
            
       view.backgroundColor = .white
       let image = UIImage(named: "test.jpg")!
           
       let testLayer = CAShapeLayer()
       testLayer.contents = image.cgImage
       testLayer.frame = view.bounds
            
       let maskLayer = CAShapeLayer()
       maskLayer.strokeColor = UIColor.black.cgColor
       maskLayer.fillColor = UIColor.black.cgColor
       maskLayer.path = UIBezierPath(rect: view.bounds).cgPath
       testLayer.mask = maskLayer
            
       view.layer.addSublayer(testLayer)
         
       /* optional for testing   
       let path = UIBezierPath()
       path.move(to: CGPoint(x: 100.0, y: 100.0))
       path.addLine(to: CGPoint(x: 200.0, y: 200.0))
       maskLayer.path = path.cgPath
       */
    }
    

    Not actually sure if a combination of stroke and fill works but maybe someone else comes up with a solution. If you want to cut out shapes from your path you could try something like this:

    override func viewDidLoad() {
       super.viewDidLoad()
            
       view.backgroundColor = .white
       let image = UIImage(named: "test.jpg")!
            
       let testLayer = CAShapeLayer()
       testLayer.contents = image.cgImage
       testLayer.frame = view.bounds
            
       let maskLayer = CAShapeLayer()
       maskLayer.fillColor = UIColor.black.cgColor
       maskLayer.fillRule = .evenOdd
       testLayer.mask = maskLayer
            
       view.layer.addSublayer(testLayer)
            
       let path = UIBezierPath(rect: CGRect(x: 100, y: 100, width: 200, height: 20))
       let fillPath = UIBezierPath(rect: view.bounds)
       fillPath.append(path)
       maskLayer.path = fillPath.cgPath
    }
    
    Login or Signup to reply.
  2. You can use a bezier path mask to "erase" the path by creating a custom CALayer subclass and overriding draw(in ctx: CGContext):

    class MyCustomLayer: CALayer {
        
        var myPath: CGPath?
        var lineWidth: CGFloat = 24.0
    
        override func draw(in ctx: CGContext) {
    
            // fill entire layer with solid color
            ctx.setFillColor(UIColor.gray.cgColor)
            ctx.fill(self.bounds);
    
            // we want to "clear" the stroke
            ctx.setStrokeColor(UIColor.clear.cgColor);
            // any color will work, as the mask uses the alpha value
            ctx.setFillColor(UIColor.white.cgColor)
            ctx.setLineWidth(self.lineWidth)
            ctx.setLineCap(.round)
            ctx.setLineJoin(.round)
            if let pth = self.myPath {
                ctx.addPath(pth)
            }
            ctx.setBlendMode(.sourceIn)
            ctx.drawPath(using: .fillStroke)
    
        }
        
    }
    

    Here’s a complete example… we’ll create a UIView subclass and, since the effect will be "scratching off the image" we’ll call it ScratchOffImageView:

    class ScratchOffImageView: UIView {
    
        public var image: UIImage? {
            didSet {
                self.scratchOffImageLayer.contents = image?.cgImage
            }
        }
    
        // adjust drawing-line-width as desired
        //  or set from
        public var lineWidth: CGFloat = 24.0 {
            didSet {
                maskLayer.lineWidth = lineWidth
            }
        }
    
        private class MyCustomLayer: CALayer {
            
            var myPath: CGPath?
            var lineWidth: CGFloat = 24.0
    
            override func draw(in ctx: CGContext) {
    
                // fill entire layer with solid color
                ctx.setFillColor(UIColor.gray.cgColor)
                ctx.fill(self.bounds);
    
                // we want to "clear" the stroke
                ctx.setStrokeColor(UIColor.clear.cgColor);
                // any color will work, as the mask uses the alpha value
                ctx.setFillColor(UIColor.white.cgColor)
                ctx.setLineWidth(self.lineWidth)
                ctx.setLineCap(.round)
                ctx.setLineJoin(.round)
                if let pth = self.myPath {
                    ctx.addPath(pth)
                }
                ctx.setBlendMode(.sourceIn)
                ctx.drawPath(using: .fillStroke)
    
            }
            
        }
    
        private let maskPath: UIBezierPath = UIBezierPath()
        private let maskLayer: MyCustomLayer = MyCustomLayer()
        private let scratchOffImageLayer: CALayer = CALayer()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() {
    
            // Important, otherwise you will get a black rectangle
            maskLayer.isOpaque = false
            
            // add the image layer
            layer.addSublayer(scratchOffImageLayer)
            // assign the layer mask
            scratchOffImageLayer.mask = maskLayer
            
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
    
            // set frames for mask and image layers
            maskLayer.frame = bounds
            scratchOffImageLayer.frame = bounds
    
            // triggers drawInContext
            maskLayer.setNeedsDisplay()
        }
    
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let touch = touches.first else { return }
            let currentPoint = touch.location(in: self)
            maskPath.move(to: currentPoint)
        }
        
        override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let touch = touches.first else { return }
            let currentPoint = touch.location(in: self)
            // add line to our maskPath
            maskPath.addLine(to: currentPoint)
            // update the mask layer path
            maskLayer.myPath = maskPath.cgPath
            // triggers drawInContext
            maskLayer.setNeedsDisplay()
        }
        
    }
    

    and, an example view controller:

    class ScratchOffViewController: UIViewController {
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = .systemBackground
            
            guard let img = UIImage(named: "test.jpg") else {
                fatalError("Could not load image!!!!")
            }
    
            let scratchOffView = ScratchOffImageView()
            
            // set the "scratch-off" image
            scratchOffView.image = img
            
            // default line width is 24.0
            //  we can set it to a different width here
            //scratchOffView.lineWidth = 12
    
            // let's add a light-gray label with red text
            //  we'll overlay the scratch-off-view on top of the label
            //  so we can see the text "through" the image
            let backgroundLabel = UILabel()
            backgroundLabel.font = .italicSystemFont(ofSize: 36)
            backgroundLabel.text = "This is some text in a label so we can see that the path is clear -- so it appears as if the image is being "scratched off""
            backgroundLabel.numberOfLines = 0
            backgroundLabel.textColor = .red
            backgroundLabel.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            
            [backgroundLabel, scratchOffView].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
                view.addSubview(v)
            }
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                backgroundLabel.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.7),
                backgroundLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                backgroundLabel.centerYAnchor.constraint(equalTo: g.centerYAnchor),
                
                scratchOffView.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.8),
                scratchOffView.heightAnchor.constraint(equalTo: scratchOffView.widthAnchor, multiplier: 2.0 / 3.0),
                scratchOffView.centerXAnchor.constraint(equalTo: backgroundLabel.centerXAnchor),
                scratchOffView.centerYAnchor.constraint(equalTo: backgroundLabel.centerYAnchor),
            ])
            
        }
    
    }
    

    It will look like this to start – I used a 3:2 image, and overlaid it on a light-gray label with red text so we can see that we are "scratching off" the image:

    enter image description here

    then, after a little bit of "scratching":

    enter image description here

    and after a lot of "scratching":

    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search