Swift bezier curve fill gradient fill - ios

Trying to create this curve in Swift using CG drawing on a view in my storyboard.
I've done the white curve but having trouble with filling it with a gradient as show in the image

There are three parts to this image.
The white curve.
The gradient.
The mask that blocks out the gradient.
Your best approach would be to create these as separate layers.
First create a CAGradientLayer with your desired colours.
Then create a path curve in white and add it above the gradient.
Now create a third layer with a closed path (similar to the curve but closed at the bottom. Use this as a maskLayer on the gradient layer.

Here is working code
class FillCurve: UIView {
override func draw(_ rect: CGRect) {
// Drawing ARC
let center = CGPoint(x: bounds.width / 2 , y: bounds.height / 2)
let curve = UIBezierPath()
curve.move(to:CGPoint(x: 10, y: bounds.height - 20))
curve.addQuadCurve(to: CGPoint(x: bounds.width - 20 , y: bounds.height - 20), controlPoint:center )
UIColor.white.setStroke()
curve.lineWidth = 2.0
curve.stroke()
let fillPath = curve.copy() as! UIBezierPath
fillPath.addLine(to: CGPoint(x: bounds.width , y: bounds.height - 2))
fillPath.addLine(to: CGPoint(x: 0, y: bounds.height - 2))
fillPath.close()
fillPath.fill()
fillPath.addClip()
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colors = [UIColor.cyan.cgColor,UIColor.black.cgColor]
let location:[CGFloat] = [0.0,1.0]
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: location)!
UIGraphicsGetCurrentContext()?.drawLinearGradient(gradient, start: CGPoint(x: 0, y: fillPath.bounds.height), end: CGPoint(x: 0, y: bounds.height), options: [])
}
}
output

Related

Why the edges of UIView are not smooth for huge corner radius?

class AttributedView: UIView {
private let gradient = CAGradientLayer()
var cornerRadius: CGFloat = 3 {
didSet {
layer.cornerRadius = cornerRadius
}
}
}
I simply use it for both: button view (corner radius: 20) and background circle (corner radius: 600).
Why button is smooth, and background is not?
With iOS 13.0 you can simple do, in addition to setting corner radius
yourView.layer.cornerCurve = .continuous
You should use bezzier paths and draw circle. After that you will receive nice, smooth edges.
let context = UIGraphicsGetCurrentContext()!
let gradient = CGGradient(colorsSpace: nil, colors: [UIColor.red.cgColor, UIColor.white.cgColor] as CFArray, locations: [0, 1])!
let ovalPath = UIBezierPath(ovalIn: CGRect(x: 64, y: 9, width: 111, height: 93))
context.saveGState()
ovalPath.addClip()
context.drawLinearGradient(gradient, start: CGPoint(x: 119.5, y: 9), end: CGPoint(x: 119.5, y: 102), options: [])
context.restoreGState()
UIBezierPath is a simple and efficient class for drawing shapes using Swift, which you can then put into CAShapeLayer, SKShapeNode, or other places. It comes with various shapes built in, so you can write code like this to create a rounded rectangle or a circle:
let rect = CGRect(x: 0, y: 0, width: 256, height: 256)
let roundedRect = UIBezierPath(roundedRect: rect, cornerRadius: 50)
let circle = UIBezierPath(ovalIn: rect)
You can also create custom shapes by moving a pen to a starting position then adding lines:
let freeform = UIBezierPath()
freeform.move(to: .zero)
freeform.addLine(to: CGPoint(x: 50, y: 50))
freeform.addLine(to: CGPoint(x: 50, y: 150))
freeform.addLine(to: CGPoint(x: 150, y: 50))
freeform.addLine(to: .zero)
If your end result needs a CGPath, you can get one by accessing the cgPath property of your UIBezierPath.
You probably should clip bounds of this view:
attributedView.clipsToBounds = true

Swift: UIBezierPath fill intersection of 2 paths

I'm working with 2 UIBezierPath to create a rectangle and a semicircle shape. It works fine but I am not able to fill the intersection with color.
That's my drawCircle and my addPaths functions:
private func drawCircle(selectedPosition: SelectionRangePosition, currentMonth: Bool) {
circleView.layer.sublayers = nil
let radius = contentView.bounds.height/2
let arcCenter = CGPoint(x: ceil(contentView.bounds.width/2), y: ceil(contentView.bounds.height/2))
let circlePath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: .pi/2, endAngle: .pi * 3/2, clockwise: selectedPosition == .left)
let semiCircleLayerPath = UIBezierPath()
let semiCircleLayer = CAShapeLayer()
semiCircleLayer.fillColor = UIColor.tealish.cgColor
circleView.layer.addSublayer(semiCircleLayer)
switch (selectedPosition, currentMonth) {
case (.left, true):
let rectanglePath = UIBezierPath(rect: CGRect(x: contentView.bounds.width / 2, y: 0, width: ceil(contentView.bounds.width / 2), height: contentView.bounds.height))
addPaths(semiCircleLayerPath, rectanglePath, circlePath, semiCircleLayer)
case (.middle, true):
backgroundColor = .tealish
case (.right, true):
// Note: The + 10 is just to overlap the shapes and test if the filling works
let rectanglePath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: ceil(contentView.bounds.width / 2) + 10, height: contentView.bounds.height))
addPaths(semiCircleLayerPath, rectanglePath, circlePath, semiCircleLayer)
default:
backgroundColor = .clear
}
}
private func addPaths(_ semiCircleLayerPath: UIBezierPath, _ rectanglePath: UIBezierPath, _ circlePath: UIBezierPath, _ semiCircleLayer: CAShapeLayer) {
semiCircleLayerPath.append(circlePath)
semiCircleLayerPath.append(rectanglePath)
semiCircleLayer.path = semiCircleLayerPath.cgPath
}
The problem is that the semicircle path and the rectangle path are being added in opposite directions. This is a function of the way the Cocoa API adds them, unfortunately; in your case, it means that the intersection has a "wrap count" of 0.
Fortunately, there is an easy fix: create separate paths for the semicircle and the rectangle. When fill them both individually, you should get your desired result.
Unfortunately, this will mean that you can't use layer drawing only: you will want to use the drawRect method to draw the background.
If your dates are separate UILabels, you can skip this rendering & path filling problem entirely by dropping a UIView as a child of the calendar, under the labels holding the dates; give it a background color & rounded corners (using layer.cornerRadius as usual). That'll take care of your rounded rect for you.

Adding the same layer in a subview changes its visual position

I added a subview (with a black border) in a view and centered it.
Then I generate 2 identical triangles with CAShapeLayer and add one to the subview and the other to the main view.
Here is the visual result in Playground where we can see that the green triangle is totally off and should have been centered.
And here is the code:
let view = UIView()
let borderedView = UIView()
var containedFrame = CGRect(x: 0, y: 0, width: 100, height: 100)
func setupUI() {
view.frame = CGRect(x: 0, y: 0, width: 300, height: 600)
view.backgroundColor = .white
borderedView.frame = containedFrame
borderedView.center = view.center
borderedView.backgroundColor = .clear
borderedView.layer.borderColor = UIColor.black.cgColor
borderedView.layer.borderWidth = 1
view.addSubview(borderedView)
setupTriangles()
}
private func setupTriangles() {
view.layer.addSublayer(createTriangle(color: .red)) // RED triangle
borderedView.layer.addSublayer(createTriangle(color: .green)) // GREEN triangle
}
private func createTriangle(color: UIColor) -> CAShapeLayer {
let layer = CAShapeLayer()
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: -containedFrame.width, y: 0))
bezierPath.addLine(to: CGPoint(x: 0, y: -containedFrame.height))
bezierPath.close()
layer.position = borderedView.center
layer.path = bezierPath.cgPath
layer.fillColor = color.cgColor
return layer
}
Note: All position (of view, the borderedView and both triangles) are the same (150.0, 300.0)
Question: Why is the green layer not in the right position?
#DuncanC is right that each view has its own coordinate system. Your problem is this line:
layer.position = borderedView.center
That sets the layer's position to the center of the frame for the borderedView which is in the coordinate system of view. When you create the green triangle, it needs to use the coordinate system of borderedView.
You can fix this by passing the view to your createTriangle function, and then use the center of the bounds of that view as the layer position:
private func setupTriangles() {
view.layer.addSublayer(createTriangle(color: .red, for: view)) // RED triangle
borderedView.layer.addSublayer(createTriangle(color: .green, for: borderedView)) // GREEN triangle
}
private func createTriangle(color: UIColor, for view: UIView) -> CAShapeLayer {
let layer = CAShapeLayer()
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: -containedFrame.width, y: 0))
bezierPath.addLine(to: CGPoint(x: 0, y: -containedFrame.height))
bezierPath.close()
layer.position = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
layer.path = bezierPath.cgPath
layer.fillColor = color.cgColor
return layer
}
Note: When you do this, the green triangle appears directly below the red one, so it isn't visible.
Every view/layer uses the coordinate system of it's superview/superlayer. If you add a layer to self.view.layer, it will be positioned in self.view.layer's coordinate system. If you add a layer to borderedView.layer, it will be in borderedView.layer's coordinate system.
Think of the view/layer hierarchy as stacks of pieces of graph paper. You place a new piece of paper on the current piece (the superview/layer) in the current piece's coordinates system, but then if you draw on the new view/layer, or add new views/layer inside that one, you use the new view/layer's coordinate system.

CATransform3DMakeRotation anchorpoint and position in Swift

Hi I am trying to rotate a triangle shaped CAShapeLayer. The northPole CAShapeLayer is defined in a custom UIView class. I have two functions in the class one to setup the layer properties and one to animate the rotation. The rotation animate works fine but after the rotation completes it reverts back to the original position. I want the layer to stay at the angle given (positionTo = 90.0) after the rotation animation.
I am trying to set the position after the transformation to retain the correct position after the animation. I have also tried to get the position from the presentation() method but this has been unhelpful. I have read many articles on frame, bounds, anchorpoints but I still cannot seem to make sense of why this transformation rotation is not working.
private func setupNorthPole(shapeLayer: CAShapeLayer) {
shapeLayer.frame = CGRect(x: self.bounds.width / 2 - triangleWidth / 2, y: self.bounds.height / 2 - triangleHeight, width: triangleWidth, height: triangleHeight)//self.bounds
let polePath = UIBezierPath()
polePath.move(to: CGPoint(x: 0, y: triangleHeight))
polePath.addLine(to: CGPoint(x: triangleWidth / 2, y: 0))
polePath.addLine(to: CGPoint(x: triangleWidth, y: triangleHeight))
shapeLayer.path = polePath.cgPath
shapeLayer.anchorPoint = CGPoint(x: 0.5, y: 1.0)
The animate function:
private func animate() {
let positionTo = CGFloat(DegreesToRadians(value: degrees))
let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
rotate.fromValue = 0.0
rotate.toValue = positionTo //CGFloat(M_PI * 2.0)
rotate.duration = 0.5
northPole.add(rotate, forKey: nil)
let present = northPole.presentation()!
print("presentation position x " + "\(present.position.x)")
print("presentation position y " + "\(present.position.y)")
CATransaction.begin()
northPole.transform = CATransform3DMakeRotation(positionTo, 0.0, 0.0, 1.0)
northPole.position = CGPoint(x: self.bounds.width / 2, y: self.bounds.height / 2)
CATransaction.commit()
}
To fix the problem, in the custom UIView class I had to override layoutSubviews() with the following:
super.layoutSubviews()
let northPoleTransform: CATransform3D = northPole.transform
northPole.transform = CATransform3DIdentity
setupNorthPole(northPole)
northPole.transform = northPoleTransform

Why drawing straight lines using Core Graphics I get "dashed" lines?

I have tried to draw an icon with the following swift code but I got "dashed" lines. I have just used moveToPoint and addLineToPoint functions. I got the same graphic glitch with other drawing too as you can see in the under images. I use Xcode 7.2 and run code on iOS 9.2.1.
#IBDesignable class DeleteButton: UIButton {
override func drawRect(rect: CGRect) {
UIColor.blackColor().setStroke()
let lineWidth = CGFloat(1)
// Draw empty circle
let frame = CGRect(x: rect.origin.x + lineWidth, y: rect.origin.y + lineWidth, width: rect.width - (lineWidth * 2), height: rect.height - (lineWidth * 2))
let path = UIBezierPath(ovalInRect: frame)
path.lineWidth = lineWidth
path.stroke()
// Draw X
let radius = (rect.width / 2) * 0.5
let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
let π = CGFloat(M_PI)
let centerWidth = radius * cos(π/4)
let centerHeight = radius * sin(π/4)
path.lineWidth = lineWidth
path.moveToPoint(CGPoint(x: CGFloat(center.x + centerWidth), y: CGFloat(center.y + centerHeight)))
path.addLineToPoint(CGPoint(x: CGFloat(center.x - centerWidth), y: CGFloat(center.y - centerHeight)))
path.stroke()
path.lineWidth = lineWidth
path.moveToPoint(CGPoint(x: center.x - centerWidth, y: center.y + centerHeight))
path.addLineToPoint(CGPoint(x: center.x + centerWidth, y: center.y - centerHeight))
path.stroke()
}
}
This is the result:
I apologize for my bad english.
I believe you are seeing the result of the button's title text drawing over the image. Check out the following demo using your code in a Playground:
We need to know the context in which that code runs. You can't just start drawing.
The usual way to draw in a view is to override the drawRect method. When you do that the system has the graphics context set up correctly with a mask that clips your drawing to the view's bounds, the coordinate system set up to the view's coordinates, etc.
If your drawing code is not in a view's drawRect method then that is your problem.

Resources