iosswiftuibezierpathimage-masking

Mask Image with custom UIBazierPath swift


I have created UIBezierPath with custom shape then I need to make it mask for image always I got empty image here is my code First I created the path, then create image and last create my mask but it is not working

here is image I need to mask it dropbox.com/s/tnxgx7g1uvb1zj7/TeethMask.png?dl=0 here is UIBazier path dropbox.com/s/nz93n1vgvj6c6y0/… I need to mask this image in this path The output is something like this https://www.dropbox.com/s/gueyhdmmdcfvyiq/image.png?dl=0

Here is ViewController class

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let tapGR = UITapGestureRecognizer(target: self, action: #selector(didTap))

        self.view.addGestureRecognizer(tapGR)
    }

    @objc func didTap(tapGR: UITapGestureRecognizer) {

        let tapPoint = tapGR.location(in: self.view)

        if #available(iOS 11.0, *) {
            let shapeView = ShapeView(origin: tapPoint)
             self.view.addSubview(shapeView)
        } else {
            // Fallback on earlier versions
        }


    }

}

Here is ShapeView class

import UIKit

@available(iOS 11.0, *)
class ShapeView: UIView {

    let size: CGFloat = 150
    let lineWidth: CGFloat = 3
    var fillColor: UIColor!
    var path: UIBezierPath!

    init(origin: CGPoint) {
        super.init(frame: CGRect(x: 0.0, y: 0.0, width: size, height: size))
        self.fillColor = randomColor()
        self.path = mouthPath()
        self.center = origin
        self.backgroundColor = UIColor.clear
    }


    func randomColor() -> UIColor {
        let hue:CGFloat = CGFloat(Float(arc4random()) / Float(UINT32_MAX))
        return UIColor(hue: hue, saturation: 0.8, brightness: 1.0, alpha: 0.8)
    }


    func mouthPath() -> UIBezierPath{
        let pointsArray = [CGPoint(x:36 , y:36 ),CGPoint(x:41 , y:36 ),CGPoint(x:45 , y:36 ),CGPoint(x:49 , y:36 ),CGPoint(x:53 , y:36 ),CGPoint(x:58 , y: 37),CGPoint(x:64 , y:37 ),CGPoint(x:69 , y:36 ),CGPoint(x:65 , y:29 ),CGPoint(x:58 , y:24 ),CGPoint(x:50 , y:22 ),CGPoint(x:42 , y:23 ),CGPoint(x:36 , y:28 ),CGPoint(x:32 , y:35 )]

        let newPath = UIBezierPath()
        let factor:CGFloat = 10
        for i in 0...pointsArray.count - 1 { // last point is 0,0
            let point = pointsArray[i]
            let currentPoint1 = CGPoint(x: point.x  * factor , y: point.y * factor)
        if i == 0 {
            newPath.move(to: currentPoint1)
        } else {
            newPath.addLine(to: currentPoint1)

            }
            }
            newPath.addLine(to: CGPoint(x: pointsArray[0].x  * factor, y: pointsArray[0].y * factor))
            newPath.close()

        let imageTemplate = UIImageView()
        imageTemplate.image =  UIImage(named: "TeethMask")
        self.addSubview(imageTemplate)
        self.bringSubviewToFront(imageTemplate)
        imageTemplate.frame = self.frame

        let mask = CAShapeLayer(layer: self.layer)
        mask.frame = newPath.bounds
        mask.fillColor = UIColor.clear.cgColor
        mask.strokeColor = UIColor.black.cgColor
        mask.path = newPath.cgPath
        mask.shouldRasterize = true
        imageTemplate.layer.mask = mask
        imageTemplate.layer.addSublayer(mask)
    }
}

Solution

  • Well, you're doing a few things wrong...

    The "teeth" image you linked:

    enter image description here

    has a native size of 461 x 259. So, I'm going to use a proportional "target" size of 200 x 112.

    First, shape layers use 0,0 at upper-left. Your original points array:

    let pointsArray = [
        CGPoint(x: 36, y: 36),
        CGPoint(x: 41, y: 36),
        CGPoint(x: 45, y: 36),
        CGPoint(x: 49, y: 36),
        CGPoint(x: 53, y: 36),
        CGPoint(x: 58, y: 37),
        CGPoint(x: 64, y: 37),
        CGPoint(x: 69, y: 36),
        CGPoint(x: 65, y: 29),
        CGPoint(x: 58, y: 24),
        CGPoint(x: 50, y: 22),
        CGPoint(x: 42, y: 23),
        CGPoint(x: 36, y: 28),
        CGPoint(x: 32, y: 35),
    ]
    

    gives this shape:

    enter image description here

    If we invert the y-coordinates:

    let pointsArray = [
        CGPoint(x: 36.0, y: 23.0),
        CGPoint(x: 41.0, y: 23.0),
        CGPoint(x: 45.0, y: 23.0),
        CGPoint(x: 49.0, y: 23.0),
        CGPoint(x: 53.0, y: 23.0),
        CGPoint(x: 58.0, y: 22.0),
        CGPoint(x: 64.0, y: 22.0),
        CGPoint(x: 69.0, y: 23.0),
        CGPoint(x: 65.0, y: 30.0),
        CGPoint(x: 58.0, y: 35.0),
        CGPoint(x: 50.0, y: 37.0),
        CGPoint(x: 42.0, y: 36.0),
        CGPoint(x: 36.0, y: 31.0),
        CGPoint(x: 32.0, y: 24.0),
    ]
    

    we get this shape:

    enter image description here

    It will be difficult to get things to "line up" correctly if your shape is offset like that, so we can "normalize" the points to start at top-left:

    let pointsArray: [CGPoint] = [
        CGPoint(x: 4.0, y: 1.0),
        CGPoint(x: 9.0, y: 1.0),
        CGPoint(x: 13.0, y: 1.0),
        CGPoint(x: 17.0, y: 1.0),
        CGPoint(x: 21.0, y: 1.0),
        CGPoint(x: 26.0, y: 0.0),
        CGPoint(x: 32.0, y: 0.0),
        CGPoint(x: 37.0, y: 1.0),
        CGPoint(x: 33.0, y: 8.0),
        CGPoint(x: 26.0, y: 13.0),
        CGPoint(x: 18.0, y: 15.0),
        CGPoint(x: 10.0, y: 14.0),
        CGPoint(x: 4.0, y: 9.0),
        CGPoint(x: 0.0, y: 2.0),
    ]
    

    resulting in:

    enter image description here

    However, we want the shape to fit the image, so we can scale the UIBezierPath to the bounds of the imageView:

        // need to scale the path to self.bounds
        let scaleW = bounds.width / pth.bounds.width
        let scaleH = bounds.height / pth.bounds.height
    
        let trans = CGAffineTransform(scaleX: scaleW, y: scaleH)
        pth.apply(trans)
    

    and we're here:

    enter image description here

    The only thing left is to use that as a mask for the image.

    I'm going to suggest subclassing UIImageView instead of UIView ... that way you can set the .image property without needing to add another view as a subview. Also, I think you'll find it much easier to manage the size of the custom, masked image in your controller code, rather than inside the custom class.

    Here is a demo view controller and a custom MouthShapeView:

    class TeethViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            let tapGR = UITapGestureRecognizer(target: self, action: #selector(didTap))
    
            self.view.addGestureRecognizer(tapGR)
        }
    
        @objc func didTap(tapGR: UITapGestureRecognizer) {
    
            let tapPoint = tapGR.location(in: self.view)
    
            if #available(iOS 11.0, *) {
    
                // make sure you can load the image
                if let img = UIImage(named: "TeethMask") {
                    // create custom ShapeView with image
                    let shapeView = MouthShapeView(image: img)
                    // if you want to use original image proportions
                    // set the width you want and calculate a proportional height
                    // based on the original image size
                    let targetWidth: CGFloat = 200.0
                    let targetHeight: CGFloat = img.size.height / img.size.width * targetWidth
                    // set the frame size
                    shapeView.frame.size = CGSize(width: targetWidth, height: targetHeight)
                    // set the frame center
                    shapeView.center = tapPoint
                    // add it
                    self.view.addSubview(shapeView)
                }
    
            } else {
                // Fallback on earlier versions
            }
    
    
        }
    }
    
    @available(iOS 11.0, *) class MouthShapeView: UIImageView {
    
        let pointsArray: [CGPoint] = [
            CGPoint(x: 4.0, y: 1.0),
            CGPoint(x: 9.0, y: 1.0),
            CGPoint(x: 13.0, y: 1.0),
            CGPoint(x: 17.0, y: 1.0),
            CGPoint(x: 21.0, y: 1.0),
            CGPoint(x: 26.0, y: 0.0),
            CGPoint(x: 32.0, y: 0.0),
            CGPoint(x: 37.0, y: 1.0),
            CGPoint(x: 33.0, y: 8.0),
            CGPoint(x: 26.0, y: 13.0),
            CGPoint(x: 18.0, y: 15.0),
            CGPoint(x: 10.0, y: 14.0),
            CGPoint(x: 4.0, y: 9.0),
            CGPoint(x: 0.0, y: 2.0),
        ]
    
        let maskLayer = CAShapeLayer()
    
        override init(image: UIImage?) {
            super.init(image: image)
            maskLayer.fillColor = UIColor.black.cgColor
        }
    
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
    
            let newPath = UIBezierPath()
    
            pointsArray.forEach { p in
                if p == pointsArray.first {
                    newPath.move(to: p)
                } else {
                    newPath.addLine(to: p)
                }
            }
    
            newPath.close()
    
            // need to scale the path to self.bounds
            let scaleW = bounds.width / newPath.bounds.width
            let scaleH = bounds.height / newPath.bounds.height
    
            let trans = CGAffineTransform(scaleX: scaleW, y: scaleH)
            newPath.apply(trans)
    
            maskLayer.path = newPath.cgPath
            layer.mask = maskLayer
    
        }
    
    }
    

    When you run that code, and tap on the view, you'll get this:

    enter image description here