With the code below, I am successfully masking part of my drawing, but it's the inverse of what I want masked. This masks the inner portion of the drawing, where I would like to mask the outer portion. Is there a simple way to invert this mask?
myPath below is a UIBezierPath.
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
CGMutablePathRef maskPath = CGPathCreateMutable();
CGPathAddPath(maskPath, nil, myPath.CGPath);
[maskLayer setPath:maskPath];
CGPathRelease(maskPath);
self.layer.mask = maskLayer;
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;
For Swift 3.0
func mask(viewToMask: UIView, maskRect: CGRect, invert: Bool = false) {
let maskLayer = CAShapeLayer()
let path = CGMutablePath()
if (invert) {
path.addRect(viewToMask.bounds)
}
path.addRect(maskRect)
maskLayer.path = path
if (invert) {
maskLayer.fillRule = kCAFillRuleEvenOdd
}
// Set the mask of the view.
viewToMask.layer.mask = maskLayer;
}
Based on the accepted answer, here's another mashup in Swift. I've made it into a function and made the invert optional
class func mask(viewToMask: UIView, maskRect: CGRect, invert: Bool = false) {
let maskLayer = CAShapeLayer()
let path = CGPathCreateMutable()
if (invert) {
CGPathAddRect(path, nil, viewToMask.bounds)
}
CGPathAddRect(path, nil, maskRect)
maskLayer.path = path
if (invert) {
maskLayer.fillRule = kCAFillRuleEvenOdd
}
// Set the mask of the view.
viewToMask.layer.mask = maskLayer;
}
Here's my Swift 4.2 solution that allows a corner radius
extension UIView {
func mask(withRect maskRect: CGRect, cornerRadius: CGFloat, inverse: Bool = false) {
let maskLayer = CAShapeLayer()
let path = CGMutablePath()
if (inverse) {
path.addPath(UIBezierPath(roundedRect: self.bounds, cornerRadius: cornerRadius).cgPath)
}
path.addPath(UIBezierPath(roundedRect: maskRect, cornerRadius: cornerRadius).cgPath)
maskLayer.path = path
if (inverse) {
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
}
self.layer.mask = maskLayer;
}
}
Swift 5
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let red = UIView(frame: view.bounds)
view.addSubview(red)
view.backgroundColor = UIColor.cyan
red.backgroundColor = UIColor.red
red.mask(CGRect(x: 50, y: 50, width: 50, height: 50), invert: true)
}
}
extension UIView{
func mask(_ rect: CGRect, invert: Bool = false) {
let maskLayer = CAShapeLayer()
let path = CGMutablePath()
if (invert) {
path.addRect(bounds)
}
path.addRect(rect)
maskLayer.path = path
if (invert) {
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
}
// Set the mask of the view.
layer.mask = maskLayer
}
}
Thanks #arvidurs
For Swift 4.2
func mask(viewToMask: UIView, maskRect: CGRect, invert: Bool = false) {
let maskLayer = CAShapeLayer()
let path = CGMutablePath()
if (invert) {
path.addRect(viewToMask.bounds)
}
path.addRect(maskRect)
maskLayer.path = path
if (invert) {
maskLayer.fillRule = .evenOdd
}
// Set the mask of the view.
viewToMask.layer.mask = maskLayer;
}
In order to invert mask you can to something like this
Here I have mask of crossed rectancles like
let crossPath = UIBezierPath(rect: cutout.insetBy(dx: 30, dy: -5))
crossPath.append(UIBezierPath(rect: cutout.insetBy(dx: -5, dy: 30)))
let crossMask = CAShapeLayer()
crossMask.path = crossPath.cgPath
crossMask.backgroundColor = UIColor.clear.cgColor
crossMask.fillRule = .evenOdd
And here I add 3rd rectancle around my crossed rectancles
so using .evenOdd it takes area that is equal to (New Rectancle - Old Crossed Rectangle) so in other words outside area of crossed rectancles
let crossPath = UIBezierPath(rect: cutout.insetBy(dx: -5, dy: -5) )
crossPath.append(UIBezierPath(rect: cutout.insetBy(dx: 30, dy: -5)))
crossPath.append(UIBezierPath(rect: cutout.insetBy(dx: -5, dy: 30)))
let crossMask = CAShapeLayer()
crossMask.path = crossPath.cgPath
crossMask.backgroundColor = UIColor.clear.cgColor
crossMask.fillRule = .evenOdd
There are many great answers, all of them using even-odd rule for resolving interior and exterior surfaces. Here is Swift 5 approach with non-zero rule:
extension UIView {
func mask(_ rect: CGRect, cornerRadius: CGFloat, invert: Bool = false) {
let maskLayer = CAShapeLayer()
let path = CGMutablePath()
if (invert) {
path.move(to: .zero)
path.addLine(to: CGPoint(x: 0, y: bounds.height))
path.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
path.addLine(to: CGPoint(x: bounds.width, y: 0))
path.addLine(to: .zero)
}
path.addPath(UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath)
maskLayer.path = path
layer.mask = maskLayer
}
}
For masking operation it is pretty straight forward. We draw rect and use it as masking path.
For inverted masking we use bounds of the view that is going to be masked. Then we draw it counter clockwise. After that we add UIBezierPath rect, which is by default drawn clockwise. That way any point p inside the rect will have one clockwise intersection and one counter clockwise intersection, leading to total winding number of zero, making that point an exterior point.
For 2023. All answers currently here seem wrong as they don't adjust the mask when the layout changes (or - just one example - when the view is animating in size or shape).
It's very simple ..
1. have a layer (perhaps just a square of color, an image, whatever)
lazy var examp: CALayer = {
let l = CALayer()
l.background = UIColor.blue.cgColor
l.mask = shapeAsMask
layer.addSublayer(l)
return l
}()
Note that examp has its mask set already.
2. Whenever you want to mask, you of course need a masking layer on hand
lazy var shapeAsMask: CAShapeLayer = {
let s = CAShapeLayer()
s.fillRule = .evenOdd
layer.addSublayer(s)
layer.mask = s
return s
}()
Note that the fillrule is set as needed.
3. Now make a shape with a bezier curve. Let's just make a circle:
override func layoutSubviews() {
super.layoutSubviews()
examp.frame = bounds
let i = bounds.width * 0.30
let thing = UIBezierPath(ovalIn: bounds.insetBy(dx: i, dy: i))
...
}
So that's a small circle in the middle of the view.
If you want to "have the shape" it's just
override func layoutSubviews() {
super.layoutSubviews()
fill.frame = bounds
let i = bounds.width * 0.30
let thing = UIBezierPath(ovalIn: bounds.insetBy(dx: i, dy: i))
shapeAsMask.path = thing.cgPath
}
If you want to "have the INVERSE OF the shape" it's just
override func layoutSubviews() {
super.layoutSubviews()
fill.frame = bounds
let i = bounds.width * 0.30
let thing = UIBezierPath(ovalIn: bounds.insetBy(dx: i, dy: i))
let p = CGMutablePath()
p.addRect(bounds)
p.addPath(thing.cgPath)
shapeAsMask.path = p
}
In short, the "negative" of this ..
let thing = UIBezierPath( ... some path
is just this:
let neg = CGMutablePath()
neg.addRect(bounds)
neg.addPath(thing.cgPath)
So that's it.
Don't forget that ...
Anytime you have a mask (or a layer!) you have to set it in layoutSubviews. (That's why layoutSubviews is named layoutSubviews !)
Related
I'm trying to achieve a feature that allows adding a shadow to a transparent button. For this, I'm creating a layer that masks the shadows inside the view. However, my shadows are clipped on the left and upper sides but not clipped on the right and lower sides.
Here is how it looks (This is not a transparent button but they're also working fine except the shadow being clipped like this.):
And here is my code for achieving this:
private func applyShadow() {
layer.masksToBounds = false
if shouldApplyShadow && shadowLayer == nil {
shadowLayer = CAShapeLayer()
let shapePath = CGPath(roundedRect: bounds, cornerWidth: cornerRadi, cornerHeight: cornerRadi, transform: nil)
shadowLayer.path = shapePath
shadowLayer.fillColor = backgroundColor?.cgColor
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowRadius = shadowRadius ?? 8
shadowLayer.shadowColor = (shadowColor ?? .black).cgColor
shadowLayer.shadowOffset = shadowOffset ?? CGSize(width: 0, height: 0)
shadowLayer.shadowOpacity = shadowOpacity ?? 0.8
layer.insertSublayer(shadowLayer!, at: 0)
/// If there's background color, there is no need to mask inner shadows.
if backgroundColor != .none && !(innerShadows ?? false) {
let maskLayer = CAShapeLayer()
maskLayer.path = { () -> UIBezierPath in
let path = UIBezierPath()
path.append(UIBezierPath(cgPath: shapePath))
path.append(UIBezierPath(rect: UIScreen.main.bounds))
path.usesEvenOddFillRule = true
return path
}().cgPath
maskLayer.fillRule = .evenOdd
shadowLayer.mask = maskLayer
}
}
}
I think that's something related to the Even-Odd Fill Rule algorithm, I'm not sure. But how can I overcome this clipping problem?
Thanks in advance.
EDIT:
This is what a transparent button with borders and text on it looks when shadow applied.. Which I don't want.
What it should look like this. No shadows inside but also a clear background color. (Except the clipped top and left sides):
I think a couple issues...
You are appending UIBezierPath(rect: UIScreen.main.bounds) to your path, but that puts the top-left corner at the top-left corner of the layer... which "clips" the top-left shadow.
If you DO have a background color, you'll need to clip those corners as well, or they will "bleed" outside the rounded corners.
Give this a try (only slightly modified). It's designated #IBDesignable so you can see how it looks in Storyboard / Interface Builder (I did not set any of the properties to inspectable -- I'll leave that up to you if you want to do so):
#IBDesignable
class MyRSButton: UIButton {
var shouldApplyShadow: Bool = true
var innerShadows: Bool?
var cornerRadi: CGFloat = 8.0
var shadowLayer: CAShapeLayer!
var shadowRadius: CGFloat?
var shadowColor: UIColor?
var shadowOffset: CGSize?
var shadowOpacity: Float?
override func layoutSubviews() {
super.layoutSubviews()
applyShadow()
}
private func applyShadow() {
// needed to prevent background color from bleeding past
// the rounded corners
cornerRadi = bounds.size.height * 0.5
layer.cornerRadius = cornerRadi
layer.masksToBounds = false
if shouldApplyShadow && shadowLayer == nil {
shadowLayer = CAShapeLayer()
let shapePath = CGPath(roundedRect: bounds, cornerWidth: cornerRadi, cornerHeight: cornerRadi, transform: nil)
shadowLayer.path = shapePath
shadowLayer.fillColor = backgroundColor?.cgColor
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowRadius = shadowRadius ?? 8
shadowLayer.shadowColor = (shadowColor ?? .black).cgColor
shadowLayer.shadowOffset = shadowOffset ?? CGSize(width: 0, height: 0)
shadowLayer.shadowOpacity = shadowOpacity ?? 0.8
layer.insertSublayer(shadowLayer!, at: 0)
/// If there's background color, there is no need to mask inner shadows.
if backgroundColor != .none && !(innerShadows ?? false) {
let maskLayer = CAShapeLayer()
maskLayer.path = { () -> UIBezierPath in
let path = UIBezierPath()
path.append(UIBezierPath(cgPath: shapePath))
// define a rect that is 80-pts wider and taller
// than the button... this will "expand" it from center
let r = bounds.insetBy(dx: -40, dy: -40)
path.append(UIBezierPath(rect: r))
path.usesEvenOddFillRule = true
return path
}().cgPath
maskLayer.fillRule = .evenOdd
shadowLayer.mask = maskLayer
}
}
}
}
Result:
I have a UIBezierPath inside my custom UIView draw(_ rect: CGRect) function. I would like to fill the path with a gradient color. Please can anybody guide me how can I do that.
I need to fill the clip with a gradient color and then stroke the path with black color.
There are some posts in SO which does not solve the problem. For example Swift: Gradient along a bezier path (using CALayers) this post guides how to draw on a layer in UIView but not in a UIBezierPath.
NB: I am working on Swift-3
To answer this question of yours,
I have a UIBezierPath inside my custom UIView draw(_ rect: CGRect)
function. I would like to fill the path with a gradient color.
Lets say you have an oval path,
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 100, height: 100))
To create a gradient,
let gradient = CAGradientLayer()
gradient.frame = path.bounds
gradient.colors = [UIColor.magenta.cgColor, UIColor.cyan.cgColor]
We need a mask layer for gradient,
let shapeMask = CAShapeLayer()
shapeMask.path = path.cgPath
Now set this shapeLayer as mask of gradient layer and add it to view's layer as subLayer
gradient.mask = shapeMask
yourCustomView.layer.addSublayer(gradient)
Update
Create a base layer with stroke and add before creating gradient layer.
let shape = CAShapeLayer()
shape.path = path.cgPath
shape.lineWidth = 2.0
shape.strokeColor = UIColor.black.cgColor
self.view.layer.addSublayer(shape)
let gradient = CAGradientLayer()
gradient.frame = path.bounds
gradient.colors = [UIColor.magenta.cgColor, UIColor.cyan.cgColor]
let shapeMask = CAShapeLayer()
shapeMask.path = path.cgPath
gradient.mask = shapeMask
self.view.layer.addSublayer(gradient)
You can do this directly in Core Graphics without using CALayer classes. Use bezierPath.addClip() to set the bezier path as the clipping region. Any subsequent drawing commands will be masked to that region.
I use this wrapper function in one of my projects:
func drawLinearGradient(inside path:UIBezierPath, start:CGPoint, end:CGPoint, colors:[UIColor])
{
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.saveGState()
defer { ctx.restoreGState() } // clean up graphics state changes when the method returns
path.addClip() // use the path as the clipping region
let cgColors = colors.map({ $0.cgColor })
guard let gradient = CGGradient(colorsSpace: nil, colors: cgColors as CFArray, locations: nil)
else { return }
ctx.drawLinearGradient(gradient, start: start, end: end, options: [])
}
The other answers here don't seem to work (At least in newer iOS versions?)
Here's a working example of a bezier path being created and the stroke being a gradient stroke, adjust the CGPoints and CGColors as needed:
CGPoint start = CGPointMake(300, 400);
CGPoint end = CGPointMake(350, 400);
CGPoint control = CGPointMake(300, 365);
UIBezierPath *path = [UIBezierPath new];
[path moveToPoint:start];
[path addQuadCurveToPoint:end controlPoint:control];
CAShapeLayer *shapeLayer = [CAShapeLayer new];
shapeLayer.path = path.CGPath;
shapeLayer.strokeColor = [UIColor whiteColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineWidth = 2.0;
//[self.view.layer addSublayer:shapeLayer];
CGRect gradientBounds = CGRectMake(path.bounds.origin.x-shapeLayer.lineWidth, path.bounds.origin.y-shapeLayer.lineWidth, path.bounds.size.width+shapeLayer.lineWidth*2.0, path.bounds.size.height+shapeLayer.lineWidth*2.0);
CAGradientLayer *gradientLayer = [CAGradientLayer new];
gradientLayer.frame = gradientBounds;
gradientLayer.bounds = gradientBounds;
gradientLayer.colors = #[(id)[UIColor redColor].CGColor, (id)[UIColor blueColor].CGColor];
gradientLayer.mask = shapeLayer;
[self.view.layer addSublayer:gradientLayer];
Swift 5 version of #AlbertRenshaw's (ObjC) answer, and wrapped in a UIView class.
What it displays looks spherical but on it's side.
A CGAffineTransform could be applied to rotate it 90ยบ for a cool effect.
import UIKit
class SphereView : UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
let centerPoint = CGPoint(x: frame.width / 2, y: frame.height / 2)
let radius = frame.height / 2
let circlePath = UIBezierPath(arcCenter: centerPoint,
radius: radius,
startAngle: 0,
endAngle: .pi * 2,
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath;
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = 2.0;
let gradientBounds = CGRectMake(
circlePath.bounds.origin.x - shapeLayer.lineWidth,
circlePath.bounds.origin.y - shapeLayer.lineWidth,
circlePath.bounds.size.width + shapeLayer.lineWidth * 2.0,
circlePath.bounds.size.height + shapeLayer.lineWidth * 2.0)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = gradientBounds;
gradientLayer.bounds = gradientBounds;
gradientLayer.colors = [UIColor.red, UIColor.blue].map { $0.cgColor }
gradientLayer.mask = shapeLayer;
self.layer.addSublayer(gradientLayer)
}
}
Usage in a UIViewController:
override func viewDidLoad() {
view.backgroundColor = .white
let sphereView = SphereView()
sphereView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(sphereView)
NSLayoutConstraint.activate([
sphereView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
sphereView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
sphereView.widthAnchor.constraint(equalToConstant: 150),
sphereView.heightAnchor.constraint(equalToConstant: 150),
])
}
I have been trying to do this animation for a couple of days now. I am trying to animate the size of my mask, basically taking a large circle (mask) and shrinking it.
#IBOutlet weak var myView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
animateMask()
}
func presentMaskScreenWithAnimation () {
//Setup Mask Layer
let bounds = myView.bounds
let maskLayer = CAShapeLayer()
maskLayer.frame = bounds
maskLayer.fillColor = UIColor.brown.cgColor
//CropCircle Out of mask Layer
let initialCircle = CGRect(x: 10, y: 10, width: 300, height: 300)
let path = UIBezierPath(roundedRect: initialCircle, cornerRadius: initialCircle.size.width/2)
path.append(UIBezierPath(rect: bounds))
maskLayer.path = path.cgPath
maskLayer.fillRule = kCAFillRuleEvenOdd
myView.layer.mask = maskLayer
//Define new circle path
let circlePath2 = CGRect(x: 50, y: 50, width: 100, height: 100)
let path2 = UIBezierPath(roundedRect: circlePath2, cornerRadius: circlePath2.size.width/2)
//Create animation
let anim = CABasicAnimation(keyPath: "path")
anim.fromValue = path.cgPath
anim.toValue = path2.cgPath
anim.duration = 3.0
anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
maskLayer.add(anim, forKey: nil)
}
This code sets up the mask and begins to animate it however it inverts the entire maskLayer, I am trying to just animate the path of the circle, Large -> Small
Start
Finished
Add path2.append(UIBezierPath(rect: bounds)) after let path2 = UIBezierPath(roundedRect: circlePath2, cornerRadius: circlePath2.size.width/2). Also do not forget to do maskLayer.path = path2.cgPath at the end, if you want the shrinking mask stays after animation.
The following is a image for which the the code does not align the centre of the UIView , white color.
CAShapeLayer *layer = [CAShapeLayer layer];
layer.anchorPoint=CGPointMake(0.5, 0.5);
layer.path=[UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, 75.0, 75.0)].CGPath;
layer.fillColor =[UIColor redColor].CGColor;
[self.shapeView.layer addSublayer:layer];
anchorPoint is not for setting the position of CAShapeLayer, use position instead.
let layer = CAShapeLayer()
layer.borderColor = UIColor.blackColor().CGColor
layer.borderWidth = 100
layer.bounds = CGRectMake(0, 0, 50, 50)
layer.position = myView.center
layer.fillColor = UIColor.redColor().CGColor
view.layer.addSublayer(layer)
Edit
Sorry for such a rough answer before, the code above may not work as you want, here is the correct answer I just come out. A important thing to note is: How you draw the oval path did effect the position of your CAShapeLayer
let layer = CAShapeLayer()
myView.layer.addSublayer(layer)
layer.fillColor = UIColor.redColor().CGColor
layer.anchorPoint = CGPointMake(0.5, 0.5)
layer.position = CGPointMake(myView.layer.bounds.midX, myView.layer.bounds.midY)
layer.path = UIBezierPath(ovalInRect: CGRectMake(-75.0 / 2, -75.0 / 2, 75.0, 75.0)).CGPath
From Apple Document,
The oval path is created in a clockwise direction (relative to the default
coordinate system)
In other word, the oval path is drawn from the edge instead of the center, so you must take into accound the radius of the oval you are drawing. In your case, you need to do this:
layer.path = UIBezierPath(ovalInRect: CGRectMake(-75.0 / 2, -75.0 / 2, 75.0, 75.0)).CGPath
Now we ensure that the center of drawn path is equal to the center of CAShapeLayer. Then we can use position property to move the CAShapeLayer as we want. The image below could help you understand.
CAShapeLayer *layer = [CAShapeLayer layer];
layer.anchorPoint=CGPointMake(0.5, 0.5);
layer.path=[UIBezierPath bezierPathWithOvalInRect:CGRectMake((self.shapeView.frame.size.width/2)-37.5, (self.shapeView.frame.size.height/2)-37.5, 75.0, 75.0)].CGPath;
//37.5 means width & height divided by 2
layer.fillColor =[UIColor redColor].CGColor;
[self.shapeView.layer addSublayer:layer];
For those still have issue with centering this approach worked for me:
1- Set the shape layer frame equal to parent view frame.
shapeLayer.frame = view.frame
2- remove the shape layer position property.
// shapeLayer.position = CGPoint(x: ,y: )
3- set x and y value of your bezier path equal to zero.
UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: self.width, height:
self.height))
Only these should be enough, get rid of the rest.
======
If that didn't work, then try this one:
#IBDesignable class YourView: UIView {
private var shapeLayer: CAShapeLayer!
override func draw(_ rect: CGRect) {
self.shapeLayer = CAShapeLayer()
self.shapeLayer.path = bezierPath.cgPath
self.shapeLayer.fillColor = UIColor.blue.cgColor
let bezierPathHeight = self.bezierPath.cgPath.boundingBox.height
let bezierPathWidth = self.bezierPath.cgPath.boundingBox.width
self.shapeLayer.setAffineTransform(CGAffineTransform.init(translationX: self.bounds.width / bezierPathWidth, y: self.bounds.height / bezierPathHeight))
self.shapeLayer.setAffineTransform(CGAffineTransform.init(scaleX: self.bounds.width / bezierPathHeight, y: self.bounds.height / bezierPathHeight))
self.layer.addSublayer(self.shapeLayer)
}
I've encounter same case like this. The final resolution is shapeLayer.needsDisplayOnBoundsChange = true and update CAShapeLayer frame in layoutSubviews, CenteredAlignShapeView can adjust when using Auto Layout.
class CenteredAlignShapeView: UIView {
public let dot = CAShapeLayer()
init() {
super.init(frame: .zero)
dot.needsDisplayOnBoundsChange = true
layer.insertSublayer(dot, at: 0)
dot.fillColor = UIColor.red.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
dot.frame = bounds
let circlePath = UIBezierPath(arcCenter: CGPoint(x: bounds.width/2, y: bounds.height/2), radius: 4, startAngle: 0, endAngle: CGFloat.pi*2, clockwise: true)
dot.path = circlePath.cgPath
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I have requirement like below image.
But, i'm confused about how to achieve this? Can i achieved it by using 3 UIImageViews or UIViews. If both then, which one is better? Finally, i have to combine three images & make one from that three images. I should be able to get touch of image too. I have no idea about this. Thanks.
Every UIView has a backing CALayer (accessible by aview.layer).
Every CALayer has a mask property, which is another CALayer. A mask allows to define a see-through shape for the layer, like a stencil.
So you need three UIImageViews, each of them has a different .layer.mask, each of these masks is a different CAShapeLayer whose .path are triangular CGPaths.
// Build a triangular path
UIBezierPath *path = [UIBezierPath new];
[path moveToPoint:(CGPoint){0, 0}];
[path addLineToPoint:(CGPoint){40, 40}];
[path addLineToPoint:(CGPoint){100, 0}];
[path addLineToPoint:(CGPoint){0, 0}];
// Create a CAShapeLayer with this triangular path
// Same size as the original imageView
CAShapeLayer *mask = [CAShapeLayer new];
mask.frame = imageView.bounds;
mask.path = path.CGPath;
// Mask the imageView's layer with this shape
imageView.layer.mask = mask;
Repeat three times.
You can use UIBezierPath and CAShapeLayer to achieve this
Step1: Copy following code
TrImageView.swift
import UIKit
protocol TriImageViewDelegate: class {
func didTapImage(image: UIImage)
}
class TriImageView:UIView {
//assumption: view width = 2 x view height
var images = [UIImage]()
var delegate:TriImageViewDelegate?
override func awakeFromNib() {
super.awakeFromNib()
//add imageviews
for i in 1...3 {
let imageView = UIImageView()
imageView.tag = i
imageView.userInteractionEnabled = true
self.addSubview(imageView)
}
//add gesture recognizer
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(TriImageView.handleTap(_:))))
}
//override drawRect
override func drawRect(rect: CGRect) {
super.drawRect(rect)
let width = rect.size.width
let height = rect.size.height
let frame = CGRect(x: 0, y: 0, width: width, height: height)
let pointA = CGPoint(x: 0, y: 0)
let pointB = CGPoint(x: width * 0.79, y: 0)
let pointC = CGPoint(x: width, y: 0)
let pointD = CGPoint(x: width * 0.534,y: height * 0.29)
let pointE = CGPoint(x: 0, y: height * 0.88)
let pointF = CGPoint(x: 0, y: height)
let pointG = CGPoint(x: width * 0.874, y: height)
let pointH = CGPoint(x: width, y: height)
let path1 = [pointA,pointB,pointD,pointE]
let path2 = [pointE,pointD,pointG,pointF]
let path3 = [pointB,pointC,pointH,pointG,pointD]
let paths = [path1,path2,path3]
for i in 1...3 {
let imageView = (self.viewWithTag(i) as! UIImageView)
imageView.image = images[i - 1]
imageView.frame = frame
addMask(imageView, points: paths[i - 1])
}
}
//Add mask to the imageview
func addMask(view:UIView, points:[CGPoint]){
let maskPath = UIBezierPath()
maskPath.moveToPoint(points[0])
for i in 1..<points.count {
maskPath.addLineToPoint(points[i])
}
maskPath.closePath()
let maskLayer = CAShapeLayer()
maskLayer.path = maskPath.CGPath
view.layer.mask = maskLayer
}
//handle tap
func handleTap(recognizer:UITapGestureRecognizer){
let point = recognizer.locationInView(recognizer.view)
for i in 1...3 {
let imageView = (self.viewWithTag(i) as! UIImageView)
let layer = (imageView.layer.mask as! CAShapeLayer)
let path = layer.path
let contains = CGPathContainsPoint(path, nil, point, false)
if contains == true {
delegate?.didTapImage(imageView.image!)
}
}
}
}
Step2: Set the custom class
Step3: Use it