I have a bezier path shape which I need to set as its parent view frame shape. I need to scale it to the correct size so it can fit in its own parent as borders.
I am using CAShapeLayer to add the bezier path to the parent view subLayer and then animate it.
class FrameView: NSObject {
var shapeLayer: CAShapeLayer!
func frameLayer(frame targetFrame: UIView) -> CAShapeLayer {
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 34.93, y: 0))
bezierPath.addCurve(to: CGPoint(x: 36.05, y: 0.2), controlPoint1: CGPoint(x: 34.79, y: 0), controlPoint2: CGPoint(x: 35.45, y: -0))
bezierPath.addLine(to: CGPoint(x: 36.16, y: 0.22))
bezierPath.addCurve(to: CGPoint(x: 38, y: 2.85), controlPoint1: CGPoint(x: 37.27, y: 0.63), controlPoint2: CGPoint(x: 38, y: 1.68))
bezierPath.addCurve(to: CGPoint(x: 38, y: 3), controlPoint1: CGPoint(x: 38, y: 3), controlPoint2: CGPoint(x: 38, y: 3))
// MORE CODE FOR BEZIERPATH ...
bezierPath.apply(CGAffineTransform(translationX: targetFrame.bounds.minX, y: targetFrame.bounds.minY))
bezierPath.apply(CGAffineTransform(scaleX: 2.0, y: 2.0))
self.shapeLayer = CAShapeLayer()
shapeLayer.path = bezierPath.cgPath
shapeLayer.fillColor = UIColor.white.cgColor
shapeLayer.strokeColor = UIColor.clear.cgColor
return shapeLayer
Below is what I'm getting. I tried some solutions in StackOverflow and didn't work for me. What I need is to set that white frame as borders of the parent view which is the transparent gray rectangle:
Found the solution,
First mistake was i was changing the UIView frame somewhere in ViewController, and it was giving me always a wrong coordinate than what i expected, so i just created the UIView in Storyboard and used its outlet.
second mistake was a wrong mathematical calculation.
to fit the shapeLayer in its parent view size, i've did this:
bezierPath.apply(CGAffineTransform(scaleX: abs(targetFrame.bounds.size.width / bezierPath.cgPath.boundingBox.width), y: abs(targetFrame.bounds.size.height / bezierPath.cgPath.boundingBox.height)))
before i tried dividing target frame and bezierRect sizes but i was not using abs() and it was giving me wrong result !
Related
How to draw a BezierPath on a button tap?
I'm practicing with BezierPaths. What I'm trying to do is draw a BezierPaths shape when a button is tapped but I'm not seeing anything on the screen when I tap the button. I'm not getting any errors just the output in the debug area shown below.
What am I missing?
Here is my code:
#IBAction func drawIt(_ sender: Any) {
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 46.5, y: 50.14))
bezierPath.addCurve(to: CGPoint(x: 77.5, y: 50.5), controlPoint1: CGPoint(x: 46.5, y: 50.14), controlPoint2: CGPoint(x: 64.74, y: 34.09))
bezierPath.addCurve(to: CGPoint(x: 94.5, y: 96.5), controlPoint1: CGPoint(x: 84.5, y: 59.5), controlPoint2: CGPoint(x: 88.29, y: 74.25))
UIColor.black.setStroke()
bezierPath.lineWidth = 1
bezierPath.stroke()
}
Debug Area:
CGContextSetStrokeColorWithColor: invalid context 0x0. If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable.
Everything was okay until you said:
bezierPath.stroke()
Up until then, you were just making a path, which is a neutral set of instructions. But when you said stroke(), you were saying: "Okay, now draw it, baby!" To which the runtime is now replying: "Uh, where? You can only draw in some kind of drawing (graphics) context, such as a UIView's graphics context, a CALayer's graphics context, or a UIImage context. You aren't in any of those. You're trying to draw in the middle of nowhere. I can't do that, and even if I could, you wouldn't see anything."
So, what's a good way to play with drawing commands? If you want to draw in response to a button click, here's the best approach:
Make a UIGraphicsImageRenderer of appropriate size.
Call the renderer's image method.
The image method takes a closure. That's where you draw!
You now have a UIImage (returned from the call to the image method).
Make a UIImageView and set its image to that image.
Stick the UIImageView into your interface.
Example:
#IBAction func drawIt(_ sender: Any) {
let r = UIGraphicsImageRenderer(size: CGSize(width: 150, height: 150))
let im = r.image { _ in
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 46.5, y: 50.14))
bezierPath.addCurve(to: CGPoint(x: 77.5, y: 50.5), controlPoint1: CGPoint(x: 46.5, y: 50.14), controlPoint2: CGPoint(x: 64.74, y: 34.09))
bezierPath.addCurve(to: CGPoint(x: 94.5, y: 96.5), controlPoint1: CGPoint(x: 84.5, y: 59.5), controlPoint2: CGPoint(x: 88.29, y: 74.25))
UIColor.black.setStroke()
bezierPath.lineWidth = 1
bezierPath.stroke()
}
let iv = UIImageView(image:im)
iv.frame.origin = CGPoint(x: 100, y: 100)
iv.layer.borderWidth = 1
self.view.addSubview(iv)
}
I have a CAShapeLayer (below) which is used to draw a bubble. Which all works fine, as per designs but when i start resizing it within a tableView with tableView.beginUpdates() and tableView.endUpdates(). I end up having the old layer in black momentarily. which is looks awful. I am really puzzled on how to solve this. It kinda feels that layers are black, and the fill paints over them like a mask.
I have tried several things as to add/removing the layer in different places. etc. but result is always this.
video to describe it
override func draw(_ rect: CGRect) {
self.layer.backgroundColor = UIColor.clear.cgColor
let fillColor: UIColor = self.shapeColor
let shapeLayer = CAShapeLayer()
shapeLayer.contentsScale = UIScreen.main.scale
let path = UIBezierPath()
shapeLayer.fillColor = fillColor.cgColor
let width = self.bounds.size.width
let height = self.bounds.size.height
path.move(to: CGPoint(x: 3.02, y: height * 0.5))
path.move(to: CGPoint(x: 3.02, y: 24.9))
path.addLine(to: CGPoint(x: 3.02, y: 14.62))
path.addLine(to: CGPoint(x: 3.02, y: 14.62))
path.addCurve(to: CGPoint(x: 17.64, y: -0), controlPoint1: CGPoint(x: 3.02, y: 6.55),controlPoint2: CGPoint(x: 9.57, y: -0))
path.addLine(to: CGPoint(x: width - 15.85, y: 0))
path.addLine(to: CGPoint(x: width - 15.85, y: 0))
path.addCurve(to: CGPoint(x: width, y: 14.62), controlPoint1: CGPoint(x: width - 7.77, y: 0), controlPoint2: CGPoint(x: width, y: 6.55))
path.addLine(to: CGPoint(x: width, y: height - 15.1))
path.addLine(to: CGPoint(x: width, y: height - 15.1))
path.addCurve(to: CGPoint(x: width - 15.85, y: height), controlPoint1: CGPoint(x: width, y: height - 7.03), controlPoint2: CGPoint(x: width - 7.77, y: height))
path.addLine(to: CGPoint(x: 17.64, y: height))
path.addLine(to: CGPoint(x: 17.64, y: height))
path.addCurve(to: CGPoint(x: 8.69, y: height - 3.56), controlPoint1: CGPoint(x: 14.39, y: height),controlPoint2: CGPoint(x: 11.24, y: height - 1.57))
path.addLine(to: CGPoint(x: 8.79, y: height - 3.46))
path.addCurve(to: CGPoint(x: 0, y: height), controlPoint1: CGPoint(x: 7.27, y: height - 1.27), controlPoint2: CGPoint(x: 3.84, y: height + 0.51))
path.addCurve(to: CGPoint(x: 3.02, y: height - 14.1), controlPoint1: CGPoint(x: 4.06, y: height - 4.22), controlPoint2: CGPoint(x: 3.02, y: height - 9.62))
path.close()
if self.reverted {
let mirrorOverXOrigin: CGAffineTransform = CGAffineTransform(scaleX: -1.0, y: 1.0);
let translate: CGAffineTransform = CGAffineTransform(translationX: width, y: 0);
// Apply these transforms to the path
path.apply(mirrorOverXOrigin)
path.apply(translate)
}
path.fill()
shapeLayer.path = path.cgPath
shapeLayer.rasterizationScale = UIScreen.main.scale
shapeLayer.shouldRasterize = true
if let bubbleLayer = self.bubbleLayer {
self.layer.replaceSublayer(bubbleLayer, with: shapeLayer)
} else {
self.layer.insertSublayer(shapeLayer, at: 0)
}
self.bubbleLayer = shapeLayer
}
shapeLayer.path = path.cgPath
shapeLayer.rasterizationScale = UIScreen.main.scale
shapeLayer.shouldRasterize = true
self.layer.insertSublayer(shapeLayer, at: 0)
self.bubbleLayer?.removeFromSuperlayer()
self.bubbleLayer = shapeLayer
}
your solution cannot work. Draw(_ :) is not called during animation block. It is draw after tableView.endUpdates(), after UITableView ends animation.
Draw(_ :) - I don't think you should use it for your needs at all.
If I can recommend you some solution:
1) In awakeFromNib:
override func awakeFromNib() {
super.awakeFromNib()
self.contentView.backgroundColor = self.shapeColor
self.contentView.layer.cornerRadius = 15
/** make left view and attach it to the left bottom anchors with fixed size and draw into this view the shape of your left arrow***/
self.leftArrowView.isHidden = self.isReverted
/** make right view and attach it to the right bottom anchors with fixed size and draw into this view the shape of your right arrow***/
self.rightArrowView.isHidden = !self.isReverted
}
2) In prepareForReuse:
override func prepareForReuse() {
super.prepareForReuse()
self.leftArrowView.isHidden = self.isReverted
self.rightArrowView.isHidden = !self.isReverted
}
And this should cause your cell will change size smoothly.
I'm trying to add a custom shape to an imageView. Please check the below images.
This is the required one:
This is what I have done so far:
I'm new to Core Graphics and I have done this so far:
private func customImageClipper(imageV: UIImageView){
let path = UIBezierPath()
let size = imageV.frame.size
print(size)
path.move(to: CGPoint(x: 0.0, y: size.height))
path.addLine(to: CGPoint(x: 0.8, y: size.height/2))
path.close()
let shape = CAShapeLayer()
shape.path = path.cgPath
imageV.layer.sublayers = [shape]
}
I'm creating a function to achieve a shape like this, but whenever I pass the imageView into this function, I can not see any change at all. I know that I have to move from points to another point to achieve this shape, but I have never done this. Any help would be appreciated. This is how I'm calling this function:
imageV.layoutIfNeeded()
customImageClipper(imageV: imageV)
P.S.: I'm not using Storyboard, I have created this programmatically.
There are many ways to create shapes using UIBezierPaths. This post here discusses the use of the draw function to create a shape.
Here is an example using your clip function within the cell.
func clip(imageView: UIView, withOffset offset: CGFloat) {
let path = UIBezierPath()
//Move to Top Left
path.move(to: .init(x: imageView.bounds.size.width * offset, y: 0))
//Draw line from Top Left to Top Right
path.addLine(to: .init(x: imageView.bounds.size.width, y: 0))
//Draw Line from Top Right to Bottom Right
path.addLine(to: .init(x: imageView.bounds.size.width * (1 - offset), y: imageView.bounds.size.height))
//Draw Line from Bottom Right to Bottom Left
path.addLine(to: .init(x: 0, y: imageView.bounds.size.height))
//Close Path
path.close()
//Create the Shape Mask for the ImageView
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.black.cgColor
imageView.layer.mask = shapeLayer
}
In this function, the offset is the amount of angle you would like on the shape, ranging from 0 to 1. (0.4) seems to work for your requirements.
This shares a lot of similarities with Apseri's answer, except I chose the route of percentages, rather than exact size. Nothing wrong with either approach, I just found it easier to understand with percentages. :)
One last note to point out, I used this function in the layoutSubviews function.
override func layoutSubviews() {
super.layoutSubviews()
imageView.layoutIfNeeded()
clip(imageView: self.imageView, withOffset: 0.4)
}
This output the following image:
Hope this helps.
Here is example of some path clipping. Of course path can be also put via parameters, and this can be applied to any view, as shown.
Before:
After (grey background is below ScrollView background):
func customImageClipper(imageV: UIView){
let path = UIBezierPath()
let size = imageV.frame.size
path.move(to: CGPoint(x: size.width/3.0, y: 0))
path.addLine(to: CGPoint(x: size.width/3.0 + 50, y: 0))
path.addLine(to: CGPoint(x: size.width/3.0, y: size.height))
path.addLine(to: CGPoint(x: size.width/3.0 - 50, y: size.height))
path.addLine(to: CGPoint(x: size.width/3.0, y: 0))
path.close()
let shape = CAShapeLayer()
shape.path = path.cgPath
shape.fillColor = UIColor.black.cgColor
imageV.layer.mask = shape
}
1- Subclassing your UIImageView
2- implement your custom drawings inside setNeedsLayout using UIBezierPath
class MyCustomImageView: UIImageView {
override func setNeedsLayout() {
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height))
path.addLineToPoint(CGPoint(x: self.frame.size.width, y: self.frame.size.height/2))
path.addLineToPoint(CGPoint(x: self.frame.size.width/2, y: 0))
path.addArcWithCenter(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2), radius: self.frame.size.width/2, startAngle:-CGFloat(M_PI_2), endAngle: CGFloat(M_PI_2), clockwise: false)
path.moveToPoint(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height))
path.closePath()
UIColor.redColor().setFill()
path.stroke()
path.bezierPathByReversingPath()
let shapeLayer = CAShapeLayer()
shapeLayer.frame = self.bounds
shapeLayer.path = path.CGPath
shapeLayer.fillColor = UIColor.redColor().CGColor
self.layer.mask = shapeLayer;
self.layer.masksToBounds = true;
}
}
Wave curved (upside) tabbar behind the center raised button. İ have created a customised class for tabbar by using reference of "https://medium.com/#philipp307/draw-a-custom-ios-tabbar-shape-27d298a7f4fa" But actually İ need to create a Wave Curved in the center of tab bar. İ have tried to play around with Bézier curve but not getting an exact behind the central button. I don't need a dip of it. İ need it behind (upside wave) my custom code and images attached.
func createPath() -> CGPath {
let height: CGFloat = 37.0
let path = UIBezierPath()
let centerWidth = self.frame.width / 2
path.move(to: CGPoint(x: 0, y: 0)) // start top left
path.addLine(to: CGPoint(x: (centerWidth - height * 2), y: 0)) // the beginning of the trough
// first curve down
path.addCurve(to: CGPoint(x: centerWidth, y: height),
controlPoint1: CGPoint(x: (centerWidth - 30), y: 0), controlPoint2: CGPoint(x: centerWidth - 35, y: height))
// second curve up
path.addCurve(to: CGPoint(x: (centerWidth + height * 2), y: 0),
controlPoint1: CGPoint(x: centerWidth + 35, y: height), controlPoint2: CGPoint(x: (centerWidth + 30), y: 0))
// complete the rect
path.addLine(to: CGPoint(x: self.frame.width, y: 0))
path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
return path.cgPath
}
tabbar design
Ok, I need to mask a horizontal UICollectionViewso it looks like a ribbon. My web dev counterpart got it done with an SVG that he used as a masking layer.
Here is my current situation:
And here is what I need (Photo from our web app):
Small detail in image spacing aside, I have an SVG like this:
Which I can successfully convert into UIBezierPath code (Paintcode):
let pathPath = UIBezierPath()
pathPath.move(to: CGPoint(x: 0.05, y: 0))
pathPath.addCurve(to: CGPoint(x: 162.02, y: 3.8), controlPoint1: CGPoint(x: 0.05, y: 2.1), controlPoint2: CGPoint(x: 72.57, y: 3.8))
pathPath.addCurve(to: CGPoint(x: 324, y: 0), controlPoint1: CGPoint(x: 251.48, y: 3.8), controlPoint2: CGPoint(x: 324, y: 2.1))
pathPath.addLine(to: CGPoint(x: 324, y: 150.2))
pathPath.addCurve(to: CGPoint(x: 162.02, y: 154), controlPoint1: CGPoint(x: 324, y: 152.3), controlPoint2: CGPoint(x: 251.48, y: 154))
pathPath.addCurve(to: CGPoint(x: 0.05, y: 150.2), controlPoint1: CGPoint(x: 72.57, y: 154), controlPoint2: CGPoint(x: 0.05, y: 152.3))
pathPath.addCurve(to: CGPoint(x: 0, y: 0.17), controlPoint1: CGPoint(x: 0.05, y: 148.1), controlPoint2: CGPoint(x: 0, y: 0.17))
pathPath.usesEvenOddFillRule = true
UIColor.lightGray.setFill()
pathPath.fill()
Now what do I do?
You should use the mask property of your UICollectionView by setting it to a view whose alpha channel indicates what part of the UICollectionView you want to mask out. In outline it will probably be something like:
// If you don't have a custom subclass of UICollectionView... you can handle the resize in the
// UICollectionViewController or whatever view contoller is handling your view.
//
// If you do have a custom subclass of UICollectionView... you could do something similar
// in layoutSubviews.
class MyViewController : UICollectionViewController {
// recreate the mask after the view lays out it's subviews
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let maskImage = createMaskingImage(size: self.view.bounds.size) {
let maskView = UIView(frame: self.view.bounds)
maskView.layer.contents = maskImage
self.view.mask = maskView
}
}
}
// create a masking image for a view of the given size
func createMaskingImage(size: CGSize) -> UIImage? {
let drawingBounds = CGRect(origin: CGPoint.zero, size: size)
UIGraphicsBeginImageContext(size)
// We're going to jump down to the level of cgContext for scaling
// You might be able to do this from the level of UIGraphics, but I don't know how so...
// Implicitly unwrapped optional, but if we can't get a CGContext we're in trouble
let cgContext = UIGraphicsGetCurrentContext()!
let maskingPath = createMaskingPath()
let pathBounds = maskingPath.bounds;
cgContext.saveGState()
// Clearing the image may not strictly be necessary
cgContext.clear(drawingBounds)
// Scale the context so that when we draw the path it fits in the drawing bounds
// Could just use "size" here instead of drawingBounds.size, but I think this makes it a
// little more explicit that we're matching up two rects
cgContext.scaleBy(x: drawingBounds.size.width / pathBounds.size.width,
y: drawingBounds.size.height / pathBounds.size.height)
cgContext.setFillColor(UIColor.lightGray.cgColor)
cgContext.addPath(maskingPath.cgPath)
cgContext.fillPath(using: .evenOdd)
cgContext.restoreGState()
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
func createMaskingPath() -> UIBezierPath {
let path = UIBezierPath()
path.move(to: CGPoint(x: 0.05, y: 0))
path.addCurve(to: CGPoint(x: 162.02, y: 3.8), controlPoint1: CGPoint(x: 0.05, y: 2.1), controlPoint2: CGPoint(x: 72.57, y: 3.8))
path.addCurve(to: CGPoint(x: 324, y: 0), controlPoint1: CGPoint(x: 251.48, y: 3.8), controlPoint2: CGPoint(x: 324, y: 2.1))
path.addLine(to: CGPoint(x: 324, y: 150.2))
path.addCurve(to: CGPoint(x: 162.02, y: 154), controlPoint1: CGPoint(x: 324, y: 152.3), controlPoint2: CGPoint(x: 251.48, y: 154))
path.addCurve(to: CGPoint(x: 0.05, y: 150.2), controlPoint1: CGPoint(x: 72.57, y: 154), controlPoint2: CGPoint(x: 0.05, y: 152.3))
path.addCurve(to: CGPoint(x: 0, y: 0.17), controlPoint1: CGPoint(x: 0.05, y: 148.1), controlPoint2: CGPoint(x: 0, y: 0.17))
return path
}
/*
This is a UICollectionView, which clips normally on the left and right,
but allows some extra space horizontally.
A typical example is you need to clip the scrolling items but you
still need to allow shadows.
*/
import Foundation
import UIKit
class CustomClipCollectionView: UICollectionView {
private lazy var extraSpaceOnBaseButStillClipSidesNormally: CALayer = {
let l = CALayer()
l.backgroundColor = UIColor.black.cgColor
return l
}()
override func layoutSubviews() {
extraSpaceOnBaseButStillClipSidesNormally.frame = bounds.insetBy(
dx: 0, dy: -10)
layer.mask = extraSpaceOnBaseButStillClipSidesNormally
super.layoutSubviews()
}
}