I'm trying to make a custom shape UIButton using mask layers and I was successful
extension UIButton {
func mask(withImage image : UIImage , frame : CGRect ){
let maskingLayer = CAShapeLayer()
maskingLayer.frame = frame
maskingLayer.contents = image.cgImage
self.layer.mask = maskingLayer
}
}
But I want to add a border (stroke) to the mask layer to indicate that this button was chosen.
I tried adding a sub layer to the mask layer
button.mask?.layer.borderColor = UIColor.black.cgColor
button.mask?.layer.borderWidth = 4
but didn't work since the button shape is still rectangle.
I know I can use CGMutablePath to define the shape
func mask(withPath path: CGMutablePath , frame : CGRect , color : UIColor) {
let mask = CAShapeLayer()
mask.frame = frame
mask.path = path
self.layer.mask = mask
let shape = CAShapeLayer()
shape.frame = self.bounds
shape.path = path
shape.lineWidth = 3.0
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = color.cgColor
self.layer.insertSublayer(shape, at: 0)
// self.layer.sublayers![0].masksToBounds = true
}
but drawing such complex shape using paths is extremly hard
Any help would be appreciated.
i was able to get CGPath from an image(.svg) using PocketSVG
but i faced another problem that the scale of the path is equal to the original SVG so i managed to scale the path to fit in frame , here is the full code :-
extension UIView {
func mask(withSvgName ImageName : String , frame : CGRect , color : UIColor){
let svgutils = SvgUtils()
let paths = svgutils.getLayerFromSVG(withImageName: ImageName)
let mask = CAShapeLayer()
mask.frame = frame
let newPath = svgutils.resizepath(Fitin: frame, path: paths[0].cgPath)
mask.path = newPath
self.layer.mask = mask
let shape = CAShapeLayer()
shape.frame = self.bounds
shape.path = newPath
shape.lineWidth = 2.0
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = color.cgColor
self.layer.insertSublayer(shape, at: 0)
}
}
and the utilities class
import Foundation
import PocketSVG
class SvgUtils{
func getLayerFromSVG(withImageName ImageName : String ) -> [SVGBezierPath]{
let url = Bundle.main.url(forResource: ImageName, withExtension: "svg")!
var paths = [SVGBezierPath]()
for path in SVGBezierPath.pathsFromSVG(at: url) {
paths.append(path)
}
return paths
}
func resizepath(Fitin frame : CGRect , path : CGPath) -> CGPath{
let boundingBox = path.boundingBox
let boundingBoxAspectRatio = boundingBox.width / boundingBox.height
let viewAspectRatio = frame.width / frame.height
var scaleFactor : CGFloat = 1.0
if (boundingBoxAspectRatio > viewAspectRatio) {
// Width is limiting factor
scaleFactor = frame.width / boundingBox.width
} else {
// Height is limiting factor
scaleFactor = frame.height / boundingBox.height
}
var scaleTransform = CGAffineTransform.identity
scaleTransform = scaleTransform.scaledBy(x: scaleFactor, y: scaleFactor)
scaleTransform.translatedBy(x: -boundingBox.minX, y: -boundingBox.minY)
let scaledSize = boundingBox.size.applying(CGAffineTransform (scaleX: scaleFactor, y: scaleFactor))
let centerOffset = CGSize(width: (frame.width - scaledSize.width ) / scaleFactor * 2.0, height: (frame.height - scaledSize.height) / scaleFactor * 2.0 )
scaleTransform = scaleTransform.translatedBy(x: centerOffset.width, y: centerOffset.height)
//CGPathCreateCopyByTransformingPath(path, &scaleTransform)
let scaledPath = path.copy(using: &scaleTransform)
return scaledPath!
}
}
and simply use it like this
button.mask(withSvgName: "your_svg_fileName", frame: button.bounds, color: UIColor.green)
Related
For example, I have a UIView and I need to make border with 3 colors.
I can do like that:
func addDividedColors(colors:[UIColor], width:CGFloat = 1) {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = CGRect(origin: .zero, size: self.bounds.size)
var colorsArray: [CGColor] = []
var locationsArray: [NSNumber] = []
for (index, color) in colors.enumerated() {
colorsArray.append(color.cgColor)
colorsArray.append(color.cgColor)
locationsArray.append(NSNumber(value: 1.0 / Double(colors.count) * Double(index)))
locationsArray.append(NSNumber(value: 1.0 / Double(colors.count) * Double(index + 1)))
}
gradientLayer.locations = locationsArray
gradientLayer.colors = colorsArray
let shape = CAShapeLayer()
shape.lineWidth = width
shape.path = UIBezierPath(roundedRect: self.bounds.insetBy(dx: width/2, dy: width/2), cornerRadius: self.cornerRadius).cgPath
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
gradientLayer.mask = shape
self.insertSublayer(gradientLayer, at: 0)
}
but then I'll see border with 4 sectors:
But It should looks like:
Is only way - to create circle with several colours, or it can be achieved by border?
Thanks!
So, I finally did this task. Here is an extension for CALayer:
func addDividedColors(colors: [UIColor], width: CGFloat = 10) {
let circlePath = UIBezierPath(ovalIn: frame)
var segments: [CAShapeLayer] = []
let segmentAngle: CGFloat = 1.0/CGFloat(colors.count)
for index in 0..<colors.count{
let circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
// start angle is number of segments * the segment angle
circleLayer.strokeStart = segmentAngle * CGFloat(index)
// end angle is the start plus one segment
circleLayer.strokeEnd = circleLayer.strokeStart + segmentAngle
circleLayer.lineWidth = width
circleLayer.strokeColor = colors[index].cgColor
circleLayer.fillColor = UIColor.clear.cgColor
// add the segment to the segments array and to the view
segments.insert(circleLayer, at: index)
addSublayer(segments[index])
}
}
And how it looks like now:
I have a CATextLayer with some CGAffineTransform. The bounds is same to parent bounds.
If I change the bounds size to text size, then position of a layer is also changed.
The red text is the layer without changing bounds.
How to calculating the position after changing bounds size? (green text)
Here is code from Playground:
import Cocoa
let frame = CGRect(origin: .zero, size: CGSize(width: 1200, height: 800))
let transform = CGAffineTransform(a: 0.811281, b: 0.584656, c: -0.484656, d: 0.611281, tx: 420.768, ty: -170.049)
func text(with color: NSColor, apply: Bool = false) -> CATextLayer {
let layer = CATextLayer()
let text = NSAttributedString(string: "valami", attributes: [NSForegroundColorAttributeName : color , NSFontAttributeName : NSFont.boldSystemFont(ofSize: 180)])
layer.string = text
layer.frame = frame
layer.bounds = frame
if apply {
layer.bounds.size = text.size()
// let translate = CGAffineTransform(translationX: -408.5, y: -12.8)
// let concate = transform.concatenating(translate)
// layer.setAffineTransform(concate)
layer.setAffineTransform(transform)
}
let border = CAShapeLayer()
border.path = CGPath(rect: layer.bounds, transform: nil)
border.fillColor = nil
border.strokeColor = color.cgColor
layer.addSublayer(border)
return layer
}
let background = CAShapeLayer()
background.path = CGPath(rect: frame, transform: nil)
background.fillColor = NSColor.white.cgColor
background.strokeColor = nil
let textLayer1 = text(with: .red)
let textLayer2 = text(with: NSColor.green.withAlphaComponent(0.5), apply: true)
let group = CALayer()
group.addSublayer(textLayer1)
group.bounds = frame
group.frame = frame
group.setAffineTransform(transform)
let view = NSView(frame: frame)
view.wantsLayer = true
view.layer = CALayer()
view.layer?.addSublayer(background)
view.layer?.addSublayer(group)
view.layer?.addSublayer(textLayer2)
view
OK, I figured out!
This is the working apply block:
if apply {
let textSize = text.size()
let widthChange = layer.bounds.width - textSize.width
let heightChange = layer.bounds.height - textSize.height
let anx = ((transform.c / 2) * heightChange) - ((transform.a / 2) * widthChange)
let any = ((transform.d / 2) * heightChange) - ((transform.b / 2) * widthChange)
let translate = CGAffineTransform(translationX: anx, y: any)
let concate = transform.concatenating(translate)
layer.setAffineTransform(concate)
layer.bounds.size = textSize
}
I am using PocketSVG for converting my SVG file to a CGPath like that:
override func awakeFromNib() {
super.awakeFromNib()
let myPath = PocketSVG.pathFromSVGFileNamed("CategoriesBar").takeUnretainedValue()
let lineWidth: CGFloat = 3.0
let insetRect = CGRectInset(bounds, lineWidth / 2.0, lineWidth / 2.0)
let boundingRect = CGPathGetPathBoundingBox(myPath)
let scale = insetRect.size.width / boundingRect.size.width
var transform = CGAffineTransformScale(CGAffineTransformMakeTranslation(-boundingRect.origin.x * scale + insetRect.origin.x, -boundingRect.origin.y * scale + insetRect.origin.y), scale, scale)
let transformedPath = CGPathCreateMutableCopyByTransformingPath(myPath, &transform)
let myShapeLayer = CAShapeLayer()
myShapeLayer.path = transformedPath
myShapeLayer.strokeColor = UIColor.redColor().CGColor
myShapeLayer.lineWidth = lineWidth
myShapeLayer.fillColor = UIColor.whiteColor().CGColor
self.layer.addSublayer(myShapeLayer)
}
The problem is that the myShapeLayer is bigger the view and it being cut off the side, I want that the width of the path (I don't care about the height) to be equals to the view's width.
How can I do that?
Thank you!
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
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 !)