iosswiftuikitcore-graphicscalayer

Make a see-through layer


I am trying to implement erase functionality on my app. For image I can do it with setting bend mode into clear while drawing image with UIGraphicsBeginImageContextWithOptions. Here is the code I have used

UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0)
guard let context = UIGraphicsGetCurrentContext() else { return }
image.draw(in: self.bounds)
context.setLineCap(.round)
context.setLineWidth(5)
context.setStrokeColor(UIColor.white.cgColor)
context.setBlendMode(.clear)
context.beginPath()
context.move(to: lastStroke.touchPoint)
context.addLine(to: currentStroke.touchPoint)
context.strokePath()
let updatedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

It works fine for images. But I want to implement it on UILabel.

One way it can be achieved is by converting UILabel into an UIImage which will increase difficulty for UILabel modification(e.g. change font, spacing). Also this will make it rester.

Is it possible to make a CALayer/ CAShapeLayer or UIView which can be see-through?

Here is an example I have seen on an app.

P.S: I am not looking for exact code, any implementation idea will be appreciable. enter image description here


Solution

  • All views in iOS have a "Backing layer", including UILabel views. (For Mac OS not all views have layers. For Mac OS NSViews, you have to set the view's wantsLayer property to true to attach a layer as the view's "backing store".)

    As such, you can install a mask layer on the view's layer. You can make that mask layer a CAShapeLayer, or a regular layer with a CGImage as it's contents.

    Don't mess with blend modes. Just add a mask layer to any layer that you want to be able to erase/un-erase, including a UILabel's layer

    The mask layer will show/hide the contents of the layer it masks. (Opaque pixels in the mask reveal the layer underneath, but clear pixels hide it.)

    If you want to be able to do freehand erasing/revealing of things like labels, I would suggest using a layer with an image in it as your mask. Draw opaque pixels into the masks image to "un-erase" the masked image, and draw with clear pixels to erase the masked image. Because you're just changing the mask, the masked image is left untouched.

    @IBOutlet label: UILabel
    var labelMask = CALayer()
    

    ...

    func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        // Set up the mask to be the same size as the label view
        labelMask.frame = label.bounds
        label.layer.mask = labelMask
        labelMask.contents = // install a UIImage's CGImage as the contents
        // The rest of your viewDidAppear code goes here...
    }
    

    Edit:

    I posted a demo app on Github it that illustrates how to mask ANY view using a CALayer. This demo app uses a CGImage to create a raster mask. You can also use a CAShapeLayer as a vector mask.

    Here is the readme from that project:


    MaskableImageView

    This project demonstrates how to use a CALayer to mask a UIView.

    It defines a custom subclass of UIImageView, MaskableView.

    The MaskableView class has a property maskLayer that contains a CALayer.

    MaskableView defines a didSet method on its bounds property so that when the view's bounds change, it resizes the mask layer to match the size of the image view.

    The MaskableView has a method installSampleMask which builds an image the same size as the image view, mostly filled with opaque black, but with a small rectangle in the center filled with black at an alpha of 0.7. The translucent center rectangle causes the image view to become partly transparent and show the view underneath.

    The demo app installs a couple of subviews into the MaskableView, a sample image of Scampers, one of my dogs, and a UILabel. It also installs an image of a checkerboard under the MaskableView so that you can see the translucent parts more easily.

    The MaskableView has properties circleRadius, maskDrawingAlpha, and drawingAction that it uses to let the user erase/un-erase the image by tapping on the view to update the mask.

    The MaskableView attaches a UIPanGestureRecognizer and a UITapGestureRecognizer to itself, with an action of gestureRecognizerUpdate. The gestureRecognizerUpdate method takes the tap/drag location from the gesture recognizer and uses it to draw a circle onto the image mask that either decreases the image mask's alpha (to partly erase pixels) or increase the image mask's alpha (to make those pixels more opaque.)

    The MaskableView's mask drawing is crude, and only meant for demonstration purposes. It draws a series of discrete circles intstead of rendering a path into the mask based on the user's drag gesture. A better solution would be to connect the points from the gesture recognizer and use them to render a smoothed curve into the mask.

    The app's screen looks like this:

    enter image description here