I try to make UIView to show zig-zag bottom edge. Something like http://www.shutterstock.com/pic-373176370/stock-vector-receipt-vector-icon-invoice-flat-illustration-cheque-shadow-bill-with-total-cost-amount-and-dollar-symbol-abstract-text-receipt-paper-isolated-on-green.html?src=zMGBKj_5etMCcRB3cKmCoA-1-2
I have method that create a path and set as mask, but it show as 1/4 of the view. Do I need to set something else? Look like a retina problem or coordinate problem, but don't sure which one.
func layoutZigZag(bounds: CGRect) -> CALayer {
let maskLayer = CAShapeLayer()
maskLayer.bounds = bounds
let path = UIBezierPath()
let width = bounds.size.width
let height = bounds.size.height
let topRight = CGPoint(x: width , y: height)
let topLeft = CGPoint(x: 0 , y: height)
let bottomRight = CGPoint(x: width , y: 0)
let bottomLeft = CGPoint(x: 0 , y: 0)
let zigzagHeight: CGFloat = 10
let numberOfZigZag = Int(floor(width / 23.0))
let zigzagWidth = width / CGFloat(numberOfZigZag)
path.move(to: topLeft)
path.addLine(to: bottomLeft)
// zigzag
var currentX = bottomLeft.x
var currentY = bottomLeft.y
for i in 0..<numberOfZigZag {
let upper = CGPoint(x: currentX + zigzagWidth / 2, y: currentY + zigzagHeight)
let lower = CGPoint(x: currentX + zigzagWidth, y: currentY)
path.addLine(to: upper)
path.addLine(to: lower)
currentX += zigzagWidth
}
path.addLine(to: topRight)
path.close()
maskLayer.path = path.cgPath
return maskLayer
}
and
let rect = CGRect(x: 0, y: 0, width: 320, height: 400)
let view = UIView(frame: rect)
view.backgroundColor = UIColor.red
let zigzag = layoutZigZag(bounds: rect)
view.layer.mask = zigzag
Path look correct
Result is 1/4 of the view
Change maskLayer.bounds = bounds to maskLayer.frame = bounds
Update:
Upside down is because of difference between the UI and CG, we are creating the path in UIBezierPath and converting that path as a CGPath (maskLayer.path = path.cgPath). First we have to know the difference, where CGPath is Quartz 2D and origin is at the bottom left while in UIBezierPath is UIKit origin is at the top-left. As per your code, applied coordinates are as per the top-left ie UIBezierPath when we transform to CGPath (origin at bottom left) it becomes upside down. so change the code as below to get the desired effect.
let topRight = CGPoint(x: width , y: 0)
let topLeft = CGPoint(x: 0 , y: 0)
let bottomLeft = CGPoint(x: 0 , y: (height - zigzagHeight))
Quartz 2D Coordinate Systems
UIBezierPath Coordinate Systems
Related
I have to create a tab with buttons and some custom border (Images below). Problem is I have to add cornerRadius for each intersect but I'm not sure how to do it properly.
I have this code to draw tabs with border:
private func drawBorder(selectedTab: UIButton) {
// The Tab frame (below one)
guard let tabContainerFrame = vTabContainer?.frame else { return }
let borderColor = selectedTab.titleColor(for: .selected)
let tabFrame = selectedTab.convert(selectedTab.bounds, to: self)
let topMargin: CGFloat = 5
let tabOrigin = CGPoint(x: tabFrame.origin.x, y: tabFrame.origin.y - topMargin)
// Make paths to draw
let path = UIBezierPath()
path.move(to: tabOrigin) // Origin (top left)
path.addLine(to: CGPoint(x: tabFrame.maxX, y: tabOrigin.y)) // -> right
path.addLine(to: CGPoint(x: tabFrame.maxX, y: tabFrame.maxY)) // -> down
if tabFrame.maxX != tabContainerFrame.maxX {
path.addLine(to: CGPoint(x: tabContainerFrame.maxX, y: tabContainerFrame.origin.y)) // -> right
}
path.addLine(to: CGPoint(x: tabContainerFrame.maxX, y: tabContainerFrame.maxY)) // -> Down
path.addLine(to: CGPoint(x: tabContainerFrame.origin.x, y: tabContainerFrame.maxY)) // -> left
path.addLine(to: CGPoint(x: tabContainerFrame.origin.x, y: tabContainerFrame.origin.y)) // -> up
if tabOrigin.x != tabContainerFrame.origin.x {
path.addLine(to: CGPoint(x: tabOrigin.x, y: tabContainerFrame.origin.y)) // -> right
}
// Close the path. This will create the last line automatically.
path.close()
// Draw
let borderLayer = CAShapeLayer()
borderLayer.path = path.cgPath
borderLayer.lineCap = kCALineCapRound
borderLayer.lineJoin = kCALineJoinBevel
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.strokeColor = borderColor?.cgColor
borderLayer.cornerRadius = 10
borderLayer.lineWidth = 2
layer.addSublayer(borderLayer)
self.borderLayer = borderLayer
}
This is the result:
As you can see, even though I add cornerRadius = 10, it just doesn't work. borderLayer.lineCap = kCALineCapRound and borderLayer.lineJoin = kCALineJoinBevel doesn't help also.
Bonus:
I'd like to have a way to implement dynamic #IBInspectable var lineCornerRadius: CGFloat = 10.
If you are using UIBezierPath to draw a shape, setting cornerRadius will have no effect on that path.
Instead, you want to use path.addCurve(to: ...) to make your rounded corners.
For example:
the green dotted line is tabFrame
pt1 is tabFrame's "left" and "top + 10" (your radius)
pt2 is tabFrame's "left + 10" and "top"
pt3 is the first curve's second "control point" - the top-left corner of tabFrame
pt4 is tabFrame's "right - 10" and "top"
pt5 is tabFrame's "right" and "top + 10"
pt6 is the second curve's second "control point" - the top-right corner of tabFrame
So
path.addCurve(to: pt2, controlPoint1: pt1, controlPoint2: pt3)
adds a curve to pt2 ... from pt1 ... with curve control point of pt3
then:
path.addLine(to: pt4)
adds a line from the current point (pt2) to pt4
then:
path.addCurve(to: pt5, controlPoint1: pt4, controlPoint2: pt6)
adds a curve to pt5 ... from pt4 ... with curve control point of pt6
The rest of your shape is normal line segments.
I'm creating a custom UIView, in which I implement its draw(rect:) method by drawing a circle with a large width using UIBezierPath, that draw a square on the top (as shown in the picture, don't consider the colors or the size). Then I try creating rotated copies of the square, to match a "settings" icon (picture 2, consider only the outer ring). To do that last thing, I need to rotate the square using a CGAffineTransform(rotationAngle:) but the problem is that this rotation's center is the origin of the frame, and not the center of the circle. How can I create a rotation around a certain point in my view?
As a demonstration of #DuncanC's answer (up voted), here is the drawing of a gear using CGAffineTransforms to rotate the gear tooth around the center of the circle:
class Gear: UIView {
var lineWidth: CGFloat = 16
let boxWidth: CGFloat = 20
let toothAngle: CGFloat = 45
override func draw(_ rect: CGRect) {
let radius = (min(bounds.width, bounds.height) - lineWidth) / 4.0
var path = UIBezierPath()
path.lineWidth = lineWidth
UIColor.white.set()
// Use the center of the bounds not the center of the frame to ensure
// this draws correctly no matter the location of the view
// (thanks #dulgan for pointing this out)
let center = CGPoint(x: bounds.maxX / 2, y: bounds.maxY / 2)
// Draw circle
path.move(to: CGPoint(x: center.x + radius, y: center.y))
path.addArc(withCenter: CGPoint(x: center.x, y: center.y), radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
path.stroke()
// Box for gear tooth
path = UIBezierPath()
let point = CGPoint(x: center.x - boxWidth / 2.0, y: center.y - radius)
path.move(to: point)
path.addLine(to: CGPoint(x: point.x, y: point.y - boxWidth))
path.addLine(to: CGPoint(x: point.x + boxWidth, y: point.y - boxWidth))
path.addLine(to: CGPoint(x: point.x + boxWidth, y: point.y))
path.close()
UIColor.red.set()
// Draw a tooth every toothAngle degrees
for _ in stride(from: toothAngle, through: 360, by: toothAngle) {
// Move origin to center of the circle
path.apply(CGAffineTransform(translationX: -center.x, y: -center.y))
// Rotate
path.apply(CGAffineTransform(rotationAngle: toothAngle * .pi / 180))
// Move origin back to original location
path.apply(CGAffineTransform(translationX: center.x, y: center.y))
// Draw the tooth
path.fill()
}
}
}
let view = Gear(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
Here it is running in a Playground:
Shift the origin of your transform,
Rotate,
Shift back
Apply your transform
Maybe would help someone. Source
extension UIBezierPath {
func rotate(degree: CGFloat) {
let bounds: CGRect = self.cgPath.boundingBox
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radians = degree / 180.0 * .pi
var transform: CGAffineTransform = .identity
transform = transform.translatedBy(x: center.x, y: center.y)
transform = transform.rotated(by: radians)
transform = transform.translatedBy(x: -center.x, y: -center.y)
self.apply(transform)
}
}
Example:
let progressLayerPath = UIBezierPath(ovalIn: CGRect(x: 0,
y: 0,
width: 70,
height: 70))
progressLayerPath.rotate(degree: -90) // <-------
progressLayer.path = progressLayerPath.cgPath
progressLayer.strokeColor = progressColor.cgColor
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.lineWidth = lineWidth
layer.addSublayer(progressLayer)
If you want a quick n dirty version using UIViews instead:
UIView * dialView = [UIView new];
dialView.frame = CGRectMake(0, localY, w, 300);
dialView.backgroundColor = [UIColor whiteColor];
[analyticsView addSubview:dialView];
float lineWidth = 6;
float lineHeight = 20.0f;
int numberOfLines = 36;
for (int n = 0; n < numberOfLines; n++){
UIView * lineView = [UIView new];
lineView.frame = CGRectMake((w-lineWidth)/2, (300-lineHeight)/2, lineHeight, lineWidth);
lineView.backgroundColor = [UIColor redColor];
[dialView addSubview:lineView];
lineView.transform = CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(360/numberOfLines*n));
lineView.transform = CGAffineTransformTranslate(lineView.transform, (150-lineHeight), 0);
}
Giving:
Where w is a holder width, and radians are calculated by:
#define DEGREES_TO_RADIANS(degrees)((M_PI * degrees)/180)
I have a CGRect which I have rotated using translation and rotation function as below:-
let ctx: CGContext = UIGraphicsGetCurrentContext()!
ctx.saveGState()
let halfWidth = (selectedCropRect?.size.width)! / 2.0
let halfHeight = (selectedCropRect?.size.height)! / 2.0
let center = CGPoint(x: (selectedCropRect?.origin.x)! + halfWidth, y: (selectedCropRect?.origin.y)! + halfHeight)
// Move to the center of the rectangle:
ctx.translateBy(x: center.x, y: center.y)
// Rotate:
ctx.rotate(by: rotationAngle!);
// Draw the rectangle centered about the center:
let rect = CGRect(x: -halfWidth, y: -halfHeight, width: (selectedCropRect?.size.width)!, height: (selectedCropRect?.size.height)!)
let path = UIBezierPath(rect: rect).cgPath
ctx.addPath(path)
ctx.restoreGState()
Now the problem is i need to get all the corner points of the rotated rect and place circular views on the corner points so that the user can drag the corner points and increase/decrease size of rect. Any idea how i can place the circular views on the 4 corner points of rotated rect?
Untested but this should help point you in the right direction
guard let rect = selectedCropRect else {
return
}
let rectCenter = CGPoint(x: rect.width/2, y: rect.height/2)
// Or whatever angle you supply
let rotAngle = CGFloat.pi/4.0
// Rotate around center of rect
/*
Instead of creating a new transform you should store a transform
inside of the object you are transforming like UIView does
*/
var transform = CGAffineTransform(translationX: rectCenter.x, y: rectCenter.y)
transform = transform.rotated(by: rotAngle)
transform = transform.translatedBy(x: -rectCenter.x, y: -rectCenter.y)
// Get transformed center
let originalCenter = rectCenter.applying(transform)
var topLeft = originalCenter
topLeft.x -= rectCenter.x
topLeft.y -= rectCenter.y
topLeft = topLeft.applying(transform)
var topRight = originalCenter
topRight.x += rectCenter.x
topRight.y -= rectCenter.y
topRight = topRight.applying(transform)
var botLeft = originalCenter
botLeft.x -= rectCenter.x
botLeft.y += rectCenter.y
botLeft = botLeft.applying(transform)
var botRight = originalCenter
botRight.x += rectCenter.x
botRight.y += rectCenter.y
botRight = botRight.applying(transform)
// Create new path using the orignal supplied rect and transform
let path = CGPath(rect: rect, transform: &transform)
let bez = UIBezierPath(cgPath: path)
// Do whatever you need with the context
I have a small code on Swift, that makes animation drawing of house. For animation drawing I use CAShapeLayer() based on UIBezierPath():
func setupDrawingLayer() {
// Stop and remove all other actions and pics on the animationLayer
clearLayer()
if let _ = animationLayer{
let pathRect: CGRect = animationLayer!.bounds.insetBy(dx: 100.0, dy: 100.0)
let bottomLeft = CGPoint(x: pathRect.minX, y: pathRect.minY)
let topLeft = CGPoint(x: pathRect.minX, y: pathRect.minY + pathRect.height * 2.0 / 3.0)
let bottomRight = CGPoint(x: pathRect.maxX, y: pathRect.minY)
let topRight = CGPoint(x: pathRect.maxX, y: pathRect.minY + pathRect.height * 2.0 / 3.0)
let roofTip = CGPoint(x: pathRect.midX, y: pathRect.maxY)
let path = UIBezierPath()
path.move(to: bottomLeft)
path.addLine(to: topLeft)
...
path.addLine(to: bottomLeft)
path.addLine(to: bottomRight)
let pathShapeLayer = CAShapeLayer()
pathShapeLayer.frame = animationLayer!.bounds
pathShapeLayer.bounds = pathRect
pathShapeLayer.isGeometryFlipped = true
pathShapeLayer.path = path.cgPath
pathShapeLayer.strokeColor = UIColor.black.cgColor
pathShapeLayer.fillColor = nil
pathShapeLayer.lineWidth = 10.0
pathShapeLayer.lineJoin = kCALineJoinBevel
animationLayer!.addSublayer(pathShapeLayer)
pathLayer = pathShapeLayer
}
}
I need to export this animation to GIF file. How can I do this?
Or may be you know some other solution, that can animate UIBezierPath() drawing with exporting to GIF?
Thank you.
I'm making an app with a rotatable pie chart. However, my pie chart rotates around (0,0) and I can't find a solution to make it rotate around its center.
Some code:
// DRAWING CODE
// Set constants for piePieces
let radius: CGFloat = CGFloat(screen.width * 0.43)
let pi: CGFloat = 3.1415926535
let sliceRad: CGFloat = 2.0 * pi / CGFloat(categoryArray.count)
var currentAngle: CGFloat = -0.5 * sliceRad - 0.5 * pi
let center: CGPoint = CGPoint(x: screen.width / 2.0, y: radius)
println("Center point: \(center)")
//container!.layer.anchorPoint = CGPoint(x: screen.width, y: radius)
// Draw all pie charts, add them to container (chartView)
for category in categoryArray {
let slice = UIView()
slice.frame = self.frame
let shapeLayer = CAShapeLayer()
let scoreIndex = CGFloat(category.calculateScoreIndex())
let sliceRadius: CGFloat = scoreIndex * radius
// Draw the path
var path:UIBezierPath = UIBezierPath()
path.moveToPoint(center)
path.addLineToPoint(CGPoint(x: center.x + sliceRadius * cos(currentAngle), y: center.y + sliceRadius * sin(currentAngle)))
path.addArcWithCenter(center, radius: sliceRadius, startAngle: currentAngle, endAngle: currentAngle + sliceRad, clockwise: true)
path.addLineToPoint(center)
path.closePath()
// For next slice, add 2*pi Rad / n categories
currentAngle += sliceRad
// Add path to shapeLayer
shapeLayer.path = path.CGPath
//shapeLayer.frame = self.frame
shapeLayer.fillColor = SurveyColors().getColor(category.categoryIndex).CGColor
shapeLayer.anchorPoint = center
// Add shapeLayer to sliceView
slice.layer.addSublayer(shapeLayer)
// Add slice to chartView
container!.addSubview(slice)
}
self.addSubview(container!)
//container!.center = center
container!.layer.anchorPoint = center
}
I sheduled a NSTimer to perform a rotation every 2 seconds (for testing purposes):
func rotate() {
let t: CGAffineTransform = CGAffineTransformRotate(container!.transform, -0.78)
container!.transform = t;
}
The pie chart rotates around (0,0). What is going wrong?
I believe a good solution to your problem would be to set a container view:
container = UIView()
container!.frame = CGRect(x: 0.0, y: 0.0, width: screen.width, height: screen.width)
container!.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
container!.backgroundColor = UIColor.clearColor()
And then add all slices to this container view:
let slice = UIView()
slice.frame = CGRect(x: 0.0 , y: 0.0 , width: container!.bounds.width, height: container!.bounds.height)
let shapeLayer = CAShapeLayer()
let scoreIndex = CGFloat(category.calculateScoreIndex())
let sliceRadius: CGFloat = scoreIndex * radius
/*
*/
container!.addSubview(slice)
Also, make sure not to add your chart as a subview to a view with auto-layout constraints (which might interfere with you CGAffineTransformRotate).