I'm implementing a drawing function for an iPad app. I'm using UIBezierPaths on CAShapeLayers for the drawing. By creating a new CAShapeLayer for each TouchesBegan event, I'm building an array of 'stacked up' CAShapeLayers that allow me to easily implement undo and redo by popping and pushing layers to and from the array. I'm also doing some interesting layer blending techniques by using some CAShapeLayer.compositingFilters. That's all working very well. My challenge is erasing.
I'm attempting to create a second array of CAShapeLayers and use them to mask the first group. I'm able to ADD TO the mask group using the same technique from above while drawing with an opaque color but I am not able to remove opaque areas from the mask group.
I thought I would be able to start the masking technique with a base layer that was opaque (black, white or whatever). Then, I had hoped to draw UIBezierPaths with UIColor.clear.cgColor and combine or composite my drawn clear path with the underlying opaque, base mask. This in effect, should "erase" that area of the mask, and hide the stacked up CAShapeLayers that I draw into. I didn't want to combine the mask layers into an image because I would lose the ability to easily undo and redo by popping and pushing on the mask array.
I've included some pseudo code below. Any pointers, help, or strategies for a solution would be much appreciated! I've been working on this for a number of weeks and I'm really stumped. I can't find any info on the strategy I'm working toward for this. Also, if I'm going at the drawing functionality incorrectly from the start and there's an easier way to draw while maintaining simple undo/redo, and add erase, please let me know. I'm totally open to adjusting my approach! Thanks in advance for any assistance.
// setup the layer hierarchy for drawing
private func setupView() {
self.mainDrawLayer = CAShapeLayer()
self.mainDrawLayer.backgroundColor = UIColor.clear.cgColor
self.layer.addSublayer(self.mainDrawLayer)
// set up the mask. add an opaque background so everything shows through to start
self.maskLayer = CALayer()
let p = UIBezierPath.init(rect: self.bounds)
self.maskShapeLayer = CAShapeLayer()
self.maskShapeLayer?.fillColor = UIColor.black.cgColor
self.maskShapeLayer?.path = p.cgPath
self.maskLayer?.addSublayer(self.maskShapeLayer!)
// apply the mask
self.layer.mask = self.maskLayer
}
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let touch = touches.first else {
return
}
var erasing = false
// setup the currentDrawLayer which captures the bezier path
self.currentDrawLayer = CAShapeLayer()
self.currentDrawLayer?.lineCap = CAShapeLayerLineCap.round
self.currentDrawLayer?.fillColor = nil
// set the ink color to use for drawing
if let ink = UserDefaults.standard.string(forKey: "ink") {
self.currentDrawLayer?.strokeColor = UIColor(hex: ink)?.cgColor
} else {
self.currentDrawLayer?.strokeColor = UIColor(hex: Constants.inkBlack3)?.cgColor
}
if UserDefaults.standard.string(forKey: "ink") == Constants.inkBlack5 {
// removing the filter makes white overlay other colors
// this is essentially erasing with a white background
self.currentDrawLayer?.compositingFilter = nil
} else {
// this blend mode ads a whole different feeling to the drawing!
self.currentDrawLayer?.compositingFilter = "darkenBlendMode"
}
// THIS IS THE ERASER COLOR!
if UserDefaults.standard.string(forKey: "ink") == Constants.inkBlack4 {
// trying erasing via drawing with clear
self.currentDrawLayer?.strokeColor = UIColor.clear.cgColor
// is there a compositingFilter available to 'combine' my clear color with the black opaque mask layer created above?
self.currentDrawLayer?.compositingFilter = "iDontHaveADamnClueIfTheresOneThatWillWorkIveTriedThemAllItSeems:)"
erasing = true
}
self.currentDrawLayer?.path = self.mainDrawPath.cgPath
if erasing {
// add the layer to the masks
self.maskLayer!.addSublayer(self.currentDrawLayer!)
} else {
// add the layer to the drawings
self.layer.addSublayer(self.currentDrawLayer!)
}
let location = touch.location(in: self)
self.ctr = 0
self.pts[0] = location
}