Mask Uiview with Uibezierpath - Stroke Only

Mask UIView with UIBezierPath - Stroke Only

A UIBezierPath has several properties that only matter when stroking the path, including lineWidth, lineCapStyle, lineJoinStyle, and miterLimit.

A CGPath has none of these properties.

Thus when you set mask.path = bezierPath.cgPath, none of the stroke properties you set on bezierPath carries over. You've extracted just the CGPath from the UIBezierPath and none of those other properties.

You need to set the stroking properties on the CAShapeLayer rather than on any path object. Thus:

mask.path = bezierPath.cgPath
mask.lineWidth = 5

You also need to set a stroke color, because the default stroke color is nil, which means don't stroke at all:

mask.strokeColor = UIColor.white.cgColor

And, because you don't want the shape layer to fill the path, you need to set the fill color to nil:

mask.fillColor = nil

Is it possible to show/stroke only a portion of a UIBezierPath?

UIBezierPath doesn't have that, but if you add the path to a CAShapeLayer, you can control the strokeStart and strokeEnd.

Try this in a playground:

let path = UIBezierPath()
path.move(to: .zero)
path.addLine(to: CGPoint(x: 10, y: 10))
path.addLine(to: CGPoint(x: 20, y: 0))
path.addLine(to: CGPoint(x: 30, y: 10))
let layer = CAShapeLayer()
layer.path = path.cgPath
layer.strokeColor = UIColor.red.cgColor
layer.fillColor = UIColor.clear.cgColor

// Note these lines
layer.strokeStart = 0.2 // start at 20% of the way
layer.strokeEnd = 0.8 // end at 80% of the way

layer.lineWidth = 2
let view = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
view.layer.addSublayer(layer)

The above produces:

Sample Image

Without the two lines that set the stroke start and end, the view looks like:

Sample Image

clip-masking uiview with CAShapeLayer and UIBezierPath

as rob mayoff said You can do this easily by setting your view's layer mask to a CAShapeLayer.

UIBezierPath *myClippingPath = ...
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = myClippingPath.CGPath;
myView.layer.mask = mask;

In Swift

let myClippingPath = UIBezierPath( ... )
let mask = CAShapeLayer()
mask.path = myClippingPath.CGPath
myView.layer.mask = mask

iOS invert mask in drawRect

With even odd filling on the shape layer (maskLayer.fillRule = kCAFillRuleEvenOdd;) you can add a big rectangle that covers the entire frame and then add the shape you are masking out. This will in effect invert the mask.

CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
CGMutablePathRef maskPath = CGPathCreateMutable();
CGPathAddRect(maskPath, NULL, someBigRectangle); // this line is new
CGPathAddPath(maskPath, nil, myPath.CGPath);
[maskLayer setPath:maskPath];
maskLayer.fillRule = kCAFillRuleEvenOdd; // this line is new
CGPathRelease(maskPath);
self.layer.mask = maskLayer;

Mask Image with custom UIBazierPath swift

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

The "teeth" image you linked:

Sample Image

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:

Sample Image

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:

Sample Image

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:

Sample Image

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:

Sample Image

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:

Sample Image

how to set a border color to a masked UIView swift

You already use CAShapeLayer(), so you no need too add view with border. just custom only CAShapeLayer() it's enough.

shapeLayer.lineWidth = 2.0 

try this with your CAShapeLayer()

How to set UIView mask with corner radius, border and shadow?

Heh, just half hour later I managed to find solution to my own problem:

let path = UIBezierPath(roundedRect: rect, byRoundingCorners: UIRectCorner.allCorners, cornerRadii: CGSize(width: radius, height: radius))

let frameLayer = CAShapeLayer()
frameLayer.path = path.cgPath
frameLayer.shadowPath = path.cgPath

frameLayer.lineWidth = borderWidth
frameLayer.strokeColor = borderColor.cgColor
frameLayer.fillColor = backgroundColor?.cgColor
frameLayer.shadowOffset = shadowOffset
frameLayer.shadowOpacity = shadowOpacity
frameLayer.shadowRadius = shadowRadius
frameLayer.shadowColor = shadowColor.cgColor

layer.mask = frameLayer
layer.insertSublayer(frameLayer, at: 0)


Related Topics



Leave a reply



Submit