1

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)
    }
}
alexkent
  • 1,556
  • 11
  • 25
yasmina
  • 13
  • 3
  • Can you please add photo how you want your image looks like? – shraddha11 Apr 06 '20 at 11:48
  • sure, here is image I need to mask it https://www.dropbox.com/s/tnxgx7g1uvb1zj7/TeethMask.png?dl=0 here is UIBazier path https://www.dropbox.com/s/nz93n1vgvj6c6y0/Screen%20Shot%202020-04-06%20at%202.10.22%20PM.png?dl=0 I need to mask this image in this path, is it cleare ? – yasmina Apr 06 '20 at 12:15
  • @yasmina - ok... what do you mean by *"need to mask"*? Are you trying to draw a path sort-of around the teeth, so only the teeth show? Or are you just trying to draw an outline around them? – DonMag Apr 06 '20 at 12:19
  • @DonMag I need the teeth inside the path dropbox.com/s/tnxgx7g1uvb1zj7/TeethMask.png?dl=0 here is path dropbox.com/s/nz93n1vgvj6c6y0/… I need to mask this image in this path, is it cleare – yasmina Apr 06 '20 at 12:21
  • @yasmina - The `func` in the code you posted is supposed to return a `UIBezierPath` but it's not returning anything. Is this part of a custom `UIView` class? Or did you copy/paste sections from a `UIViewController` class? Edit your question and post the full class code that you are trying to use. – DonMag Apr 06 '20 at 12:58
  • @DonMag I edit my question and 2 classes added – yasmina Apr 06 '20 at 13:11
  • Still not quite clear what you're going for... do you want it to look like **(A)** https://i.stack.imgur.com/mSbh6.png or **(B)** https://i.stack.imgur.com/ERqMg.png ?? Or something else? – DonMag Apr 06 '20 at 13:39
  • @DonMag exactly like https://i.stack.imgur.com/ERqMg.png – yasmina Apr 06 '20 at 13:49

1 Answers1

0

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

DonMag
  • 69,424
  • 5
  • 50
  • 86