UIBezierPath not appearing - ios

I try to draw a separator for slider, which must be at position of 1/3 of slider length. The slider body draws successfully, buy separator - not, it doesn't show.
Code is following
class RangeSliderTrackLayer:CALayer {
weak var rangeSlider:RangeSlider?
override func drawInContext(ctx: CGContext) {
if let slider = rangeSlider {
let cornerRadius = bounds.height * 1 / 2.0
let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
CGContextAddPath(ctx, path.CGPath)
CGContextSetFillColorWithColor(ctx, UIColor.lightGrayColor().CGColor)
CGContextAddPath(ctx, path.CGPath)
CGContextFillPath(ctx)
CGContextSetFillColorWithColor(ctx, UIColor.yellowColor().CGColor)
let lowerValuePosition = CGFloat(40)
let upperValuePosition = CGFloat(80)
let rect = CGRect(x: lowerValuePosition, y: 0.0, width: upperValuePosition - lowerValuePosition, height: bounds.height)
CGContextFillRect(ctx, rect)
let separatorPath = UIBezierPath()
var x = bounds.width / 3
var y = bounds.height
separatorPath.moveToPoint(CGPoint(x: x, y: y))
separatorPath.addLineToPoint(CGPoint(x: x + 2, y: y))
separatorPath.addLineToPoint(CGPoint(x: x + 2, y: 0))
separatorPath.addLineToPoint(CGPoint(x: x, y: 0))
separatorPath.closePath()
UIColor.whiteColor().setFill()
separatorPath.stroke()
}
}
}
What am I doing wrong ?

You are calling setFill() but then then calling stroke(). Fill and stroke are two separate things. So, you either want:
Go ahead and set the fill color with setFill(), but then call fill() instead of stroke():
let separatorPath = UIBezierPath()
var x = bounds.width / 3
var y = bounds.height
separatorPath.moveToPoint(CGPoint(x: x, y: y))
separatorPath.addLineToPoint(CGPoint(x: x + 2, y: y))
separatorPath.addLineToPoint(CGPoint(x: x + 2, y: 0))
separatorPath.addLineToPoint(CGPoint(x: x, y: 0))
separatorPath.closePath()
UIColor.whiteColor().setFill()
// separatorPath.stroke()
separatorPath.fill()
Or call stroke() like you are not, but instead of calling setFill(), instead set lineWidth and call setStroke():
let separatorPath = UIBezierPath()
var x = bounds.width / 3
var y = bounds.height
separatorPath.moveToPoint(CGPoint(x: x, y: y))
separatorPath.addLineToPoint(CGPoint(x: x + 2, y: y))
separatorPath.addLineToPoint(CGPoint(x: x + 2, y: 0))
separatorPath.addLineToPoint(CGPoint(x: x, y: 0))
separatorPath.closePath()
// UIColor.whiteColor().setFill()
UIColor.whiteColor().setStroke()
separatorPath.lineWidth = 1
separatorPath.stroke()

Related

Animate tab bar shape created using UIBezierPath() based on selected index

I have created a tab Bar shape using UIBezierPath(). Im new to creating shapes so in the end I got the desired shape closer to what I wanted, not perfect but it works. It's a shape which has a wave like top, so on the left side there is a crest and second half has a trough. This is the code that I used to create the shape:
func createPath() -> CGPath {
let height: CGFloat = 40.0 // Height of the wave-like curve
let extraHeight: CGFloat = -20.0 // Additional height for top left and top right corners
let path = UIBezierPath()
let width = self.frame.width
// Creating a wave-like top edge for tab bar starting from left side
path.move(to: CGPoint(x: 0, y: extraHeight)) // Start at top left corner with extra height
path.addQuadCurve(to: CGPoint(x: width/2, y: extraHeight), controlPoint: CGPoint(x: width/4, y: extraHeight - height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width*3/4, y: extraHeight + height))
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
}
Above im using -20 so that shape stays above bounds of tab bar and second wave's trough stays above the icons of tab bar. Here is the desired result:
This was fine until I was asked to animate the shape on pressing tab bar items. So if I press second item, the crest should be above second item and if fourth, then it should be above fourth item. So I created a function called updateShape(with selectedIndex: Int) and called it in didSelect method of my TabBarController. In that im passing index of the selected tab and based on that creating new path and removing old and replacing with new one. Here is how im doing it:
func updateShape(with selectedIndex: Int) {
let shapeLayer = CAShapeLayer()
let height: CGFloat = 40.0
let extraHeight: CGFloat = -20.0
let width = self.frame.width
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: extraHeight))
if selectedIndex == 0 {
path.addQuadCurve(to: CGPoint(x: width / 2, y: extraHeight), controlPoint: CGPoint(x: width / 4, y: extraHeight - height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width / 2 + width / 4, y: extraHeight + height))
}
else if selectedIndex == 1 {
path.addQuadCurve(to: CGPoint(x: width / 2 + width / 4, y: extraHeight), controlPoint: CGPoint(x: width / 4 + width / 4, y: extraHeight - height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width * 3 / 4 + width / 4, y: extraHeight + height))
}
else if selectedIndex == 2 {
let xShift = width / 4
path.addQuadCurve(to: CGPoint(x: width / 2 + xShift, y: extraHeight), controlPoint: CGPoint(x: width / 8 + xShift, y: extraHeight + height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width * 7 / 8 + xShift, y: extraHeight - height))
}
else {
path.addQuadCurve(to: CGPoint(x: width / 2, y: extraHeight), controlPoint: CGPoint(x: width / 4, y: extraHeight + height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width / 2 + width / 4, y: extraHeight - height))
}
path.addLine(to: CGPoint(x: width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.secondarySystemBackground.cgColor
if let oldShapeLayer = self.shapeLayer {
self.layer.replaceSublayer(oldShapeLayer, with: shapeLayer)
} else {
self.layer.insertSublayer(shapeLayer, at: 0)
}
self.shapeLayer = shapeLayer
}
And calling it like this:
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBar = tabBarController.tabBar as! CustomTabBar
guard let index = viewControllers?.firstIndex(of: viewController) else {
return
}
tabBar.updateShape(with: index)
}
This is working fine as you can see below but the problem is I learned creating shapes just now and creating that wave based on width of screen so the crest and trough are exactly half the width of frame so I was able to do it for FourthViewController too and got this:
But the problem arises for remaining 2 indices. Im not able to create same wave which looks like the crest is moving above second or third item instead I get something like this:
It doesn't look like other to waves showing the hump over third item. Also my code is strictly based on 4 items and was wondering if Im asked to add 1 more item so tab bar has 5 or 6 items, it would be trouble. Is there any way to update my function that creates new shapes based on index of tab bar or can anyone help me just create shapes for remaining two items? The shape should look same just the hump should exactly be over the selected item.
So you want something like this:
Here's how I did it. First, I made a WaveView that draws this curve:
Notice that the major peak exactly in the center. Here's the source code:
class WaveView: UIView {
var trough: CGFloat {
didSet { setNeedsDisplay() }
}
var color: UIColor {
didSet { setNeedsDisplay() }
}
init(trough: CGFloat, color: UIColor = .white) {
self.trough = trough
self.color = color
super.init(frame: .zero)
contentMode = .redraw
isOpaque = false
}
override func draw(_ rect: CGRect) {
guard let gc = UIGraphicsGetCurrentContext() else { return }
let bounds = self.bounds
let size = bounds.size
gc.translateBy(x: bounds.origin.x, y: bounds.origin.y)
gc.move(to: .init(x: 0, y: size.height))
gc.saveGState(); do {
// Transform the geometry so the bounding box of the curve
// is (-1, 0) to (+1, +1), with the y axis going up.
gc.translateBy(x: bounds.midX, y: trough)
gc.scaleBy(x: bounds.size.width * 0.5, y: -trough)
// Now draw the curve.
for x in stride(from: -1, through: 1, by: 2 / size.width) {
let y = (cos(2.5 * .pi * x) + 1) / 2 * (1 - x * x)
gc.addLine(to: .init(x: x, y: y))
}
}; gc.restoreGState()
// The geometry is restored.
gc.addLine(to: .init(x: size.width, y: size.height))
gc.closePath()
gc.setFillColor(UIColor.white.cgColor)
gc.fillPath()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
Then, I made a subclass of UITabBarController named WaveTabBarController that creates a WaveView and puts it behind the tab bar. It makes the WaveView exactly twice the width of the tab bar, and sets the x coordinate of the WaveView's frame such that the peak is above the selected tab item. Here's the source code:
class WaveTabBarController: UITabBarController {
let waveView = WaveView(trough: 20)
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
if superclass!.instancesRespond(to: #selector(UITabBarDelegate.tabBar(_:didSelect:))) {
super.tabBar(tabBar, didSelect: item)
}
view.setNeedsLayout()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let shouldAnimate: Bool
if waveView.superview != view {
view.insertSubview(waveView, at: 0)
shouldAnimate = false
} else {
shouldAnimate = true
}
let tabBar = self.tabBar
let w: CGFloat
if let selected = tabBar.selectedItem, let items = tabBar.items {
w = (CGFloat(items.firstIndex(of: selected) ?? 0) + 0.5) / CGFloat(items.count) - 1
} else {
w = -1
}
let trough = waveView.trough
let tabBarFrame = view.convert(tabBar.bounds, from: tabBar)
let waveFrame = CGRect(
x: tabBarFrame.origin.x + tabBarFrame.size.width * w,
y: tabBarFrame.origin.y - trough,
width: 2 * tabBarFrame.size.width,
height: tabBarFrame.size.height + trough
)
guard waveFrame != waveView.frame else {
return
}
if shouldAnimate {
// Don't animate during the layout pass.
DispatchQueue.main.async { [waveView] in
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) {
waveView.frame = waveFrame
}
}
} else {
waveView.frame = waveFrame
}
}
}

Code not executing after end of UIView.animate?

I am in the draw function of an UIView.
I want to draw a triangle, then after an animation, it's a mirror copy.
I can draw the triangle and the animation, or the triangle and it's copy, but when I add the code for drawing the copy in the "finished in" portion of the animation, it isn't drawn.
I don't know how to solve this problem...
Here is my code (with drawing of the mirror copy not showing anything).
public override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let viewWidth = self.bounds.width
let viewHeight = self.bounds.height
context.setLineWidth(1.0)
context.setStrokeColor(UIColor.darkGray.cgColor)
context.beginPath()
let path = CGMutablePath()
let r = sqrt(viewHeight * viewHeight + viewWidth * viewWidth) * 0.50
let p1 = CGPoint(x: viewWidth/2, y: 20 + r)
var angle = CGFloat.pi * (90.0 + 30.0) / 180.0
let p2 = CGPoint(x: p1.x + cos(angle) * r, y: p1.y - sin(angle) * r)
angle = CGFloat.pi * 90.0 / 180.0
let p3 = CGPoint(x: p1.x + cos(angle) * r, y: p1.y - sin(angle) * r)
path.move(to: p1)
path.addLine(to: p2)
path.addLine(to: p3)
path.closeSubpath()
context.addPath(path)
context.strokePath()
let trf = CGAffineTransform(scaleX: -1, y: 1)
let sv = UIView(frame:self.bounds)
self.addSubview(sv)
let shapeLayer = CAShapeLayer.init()
shapeLayer.path = path
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 1.0
sv.layer.addSublayer(shapeLayer)
shapeLayer.display()
UIView.animate(withDuration: 0.5, delay: 0, options: [],
animations: {
sv.transform = trf
}) { finished in
sv.removeFromSuperview()
context.setStrokeColor(UIColor.red.cgColor)
context.scaleBy(x: -1, y: 1)
context.translateBy(x:-viewWidth, y:0)
context.addPath(path)
context.strokePath()
context.scaleBy(x: -1, y: 1)
context.translateBy(x:viewWidth, y:0)
}
}

SpriteKit: Square to Circle animation using SKShapeNode UIBezierPath rounded corners

I'm trying to smoothly animate a square into a circle in SpriteKit.
I'm creating the SKShape with a UIBezierPath using rounded corners. Then, I vary the corner radii to animate.
My problem is that I seem to have a jump in the animation, please see the gif below. Preferably using the rounded corners technique, how can I get it to be smooth?
"Jumpy" problem
let shape = SKShapeNode()
let l: CGFloat = 100.0
shape.path = UIBezierPath(roundedRect: CGRect(x: -l/2, y: -l/2, width: l, height: l), byRoundingCorners: [.topLeft, .bottomLeft, .topRight, .bottomRight], cornerRadii: CGSize(width: 0, height: 0)).cgPath
shape.position = CGPoint(x: frame.midX, y: frame.midY)
shape.fillColor = .white
addChild(shape)
let action = SKAction.customAction(withDuration: 1) { (node, t) in
let shapeNode = node as! SKShapeNode
shapeNode.path = UIBezierPath(roundedRect: CGRect(x: -l/2, y: -l/2, width: l, height: l), byRoundingCorners: [.topLeft, .bottomLeft, .topRight, .bottomRight], cornerRadii: CGSize(width: t * l / 2, height: 0)).cgPath
}
shape.run(SKAction.repeatForever(action))
Animation debugging
To debug I have created some shapes with progressively larger corner radii as you can see below. The numbers represent the ratio of the corner radii to the length of the square. As you can see there is a jump between 0.3 and 0.35. I can't see what I'm missing.
let cols = 10
let rows = 1
let l: Double = 30.0
let max: Double = l / 2
let delta: Double = l * 2
for i in 0..<rows * cols {
let s = SKShapeNode()
let c: Double = Double(i % cols)
let r: Double = floor(Double(i) / Double(cols))
let pct: Double = Double(i) / (Double(rows) * Double(cols))
let rad = pct * max
s.path = UIBezierPath(roundedRect: CGRect(x: -l/2, y: -l/2, width: l, height: l), byRoundingCorners: [.topRight, .bottomRight, .topLeft, .bottomLeft], cornerRadii: CGSize(width: pct * max, height: pct * max)).cgPath
s.position = CGPoint(x: c * delta - Double(cols) / 2.0 * delta, y: r * delta - Double(rows) / 2.0 * delta)
s.lineWidth = 1.5
s.strokeColor = .white
addChild(s)
let t = SKLabelNode(text: String(format:"%0.2f", rad / l))
t.verticalAlignmentMode = .center
t.horizontalAlignmentMode = .center
t.fontName = "SanFrancisco-Bold"
t.fontSize = 15
t.position = CGPoint(x: 0, y: -delta * 0.66)
s.addChild(t)
}
You may not find the answer using a current api. But you can draw it by yourself.
let duration = 10.0
let action = SKAction.customAction(withDuration: duration) { (node, t) in
let shapeNode = node as! SKShapeNode
let path = CGMutablePath()
let borderRadius = l/2 * t / CGFloat(duration);
path.move(to: CGPoint.init(x: -l/2, y: -l/2 + borderRadius));
path.addLine(to: CGPoint.init(x: -l/2, y: l/2 - borderRadius));
path.addArc(tangent1End: CGPoint(x: -l/2, y: l/2), tangent2End: CGPoint(x: -l/2 + borderRadius, y: l/2), radius: borderRadius)
path.addLine(to: CGPoint.init(x: l/2 - borderRadius, y: l/2 ));
path.addArc(tangent1End: CGPoint(x: l/2, y: l/2), tangent2End: CGPoint(x: l/2, y: l/2 - borderRadius), radius: borderRadius)
path.addLine(to: CGPoint.init(x: l/2, y: -l/2 + borderRadius));
path.addArc(tangent1End: CGPoint(x: l/2, y: -l/2), tangent2End: CGPoint(x: l/2 - borderRadius, y: -l/2), radius: borderRadius)
path.addLine(to: CGPoint.init(x: -l/2 + borderRadius, y: -l/2));
path.addArc(tangent1End: CGPoint(x: -l/2, y: -l/2), tangent2End: CGPoint(x: -l/2, y: -l/2 + borderRadius), radius: borderRadius)
path.closeSubpath()
shapeNode.path = path
}
The documentation of UIBezierPath.init(roundedRect:cornerRadius:) states that:
The radius of each corner oval. A value of 0 results in a rectangle without rounded corners. Values larger than half the rectangle’s width or height are clamped appropriately to half the width or height.
But I tested this and it is actually happening exactly for values larger than a third of the width/height and not at a half of it. I'm filling a bug report and will let's hope it is quickly solved.
Bug report link: https://bugs.swift.org/browse/SR-10496

gradient in for multipel UIBezierPath

I'm supposed to create this.
I did search the google, youtube, and StackOverflow, and the code below is the result of my research.
#IBDesignable class TriangleView2: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
let gradient = CAGradientLayer()
override func draw(_ rect: CGRect) {
//draw the line of UIBezierPath
let path1 = UIBezierPath()
path1.move(to: CGPoint(x: rect.minX - 100, y: rect.maxY - 80))
path1.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path1.addLine(to: CGPoint(x: (rect.maxX + 90 ), y: rect.minY/2 ))
path1.close()
// add clipping path. this draws an imaginary line (to create bounds) from the
//ends of the UIBezierPath line down to the bottom of the screen
let clippingPath = path1.copy() as! UIBezierPath
clippingPath.move(to: CGPoint(x: rect.minX - 100, y: rect.maxY - 80))
clippingPath.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
clippingPath.addLine(to: CGPoint(x: (rect.maxX + 90 ), y: rect.minY/2 ))
clippingPath.close()
clippingPath.addClip()
// create and add the gradient
let colors = [theme.current.profile_start_view1.cgColor, theme.current.profile_end_view1.cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colorLocations:[CGFloat] = [0.0, 1.0]
let gradient = CGGradient(colorsSpace: colorSpace,
colors: colors as CFArray,
locations: colorLocations)
let context = UIGraphicsGetCurrentContext()
let startPoint = CGPoint(x: 1, y: 1)
let endPoint = CGPoint(x: 1, y: bounds.maxY)
// and lastly, draw the gradient.
context!.drawLinearGradient(gradient!, start: startPoint, end:
endPoint, options: CGGradientDrawingOptions.drawsAfterEndLocation)
}
}
Right not I have 2 views ( will be 3 if I could complete it) with some differences.
The result is this.
These 2 views do not have the same colour, but as you can see both views have the same gradient with the same direction.
Does anyone have any suggestion?
This is somewhat similar to Codo's answer but you only need 4 points.
class FourGradientsView: UIView {
override func draw(_ rect: CGRect) {
let ctx = UIGraphicsGetCurrentContext()!
// Points of area to draw - adjust these 4 variables as needed
let tl = CGPoint(x: 0, y: 0)
let tr = CGPoint(x: bounds.width * 1.3, y: 0)
let bl = CGPoint(x: -bounds.width * 1.8, y: bounds.height * 1.4)
let br = CGPoint(x: bounds.width * 1.3, y: bounds.height * 2)
// Find the intersection of the two crossing diagonals
let s1x = br.x - tl.x
let s1y = br.y - tl.y
let s2x = tr.x - bl.x
let s2y = tr.y - bl.y
//let s = (-s1y * (tl.x - bl.x) + s1x * (tl.y - bl.y)) / (-s2x * s1y + s1x * s2y)
let t = ( s2x * (tl.y - bl.y) - s2y * (tl.x - bl.x)) / (-s2x * s1y + s1x * s2y)
let center = CGPoint(x: tl.x + (t * s1x), y: tl.y + (t * s1y))
// Create clipping region to avoid drawing where we don't want any gradients
ctx.saveGState()
let clip = CGPoint(x: 0, y: bounds.height * 0.7)
let clipPath = UIBezierPath()
clipPath.move(to: CGPoint(x: 0, y: 0))
clipPath.addLine(to: clip)
clipPath.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
clipPath.addLine(to: CGPoint(x: bounds.width, y: 0))
clipPath.close()
clipPath.addClip()
// Use these two colors for all 4 gradients (adjust as needed)
let colors = [
UIColor(hue: 120/360, saturation: 1, brightness: 0.85, alpha: 1).cgColor,
UIColor(hue: 120/360, saturation: 1, brightness: 0.3, alpha: 1).cgColor
] as CFArray
// The common gradient
let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors, locations: nil)!
// Top gradient
ctx.saveGState()
let pathTop = UIBezierPath()
pathTop.move(to: tl)
pathTop.addLine(to: tr)
pathTop.addLine(to: center)
pathTop.close()
pathTop.addClip()
ctx.drawLinearGradient(gradient, start: CGPoint(x: bounds.width, y: 0), end: CGPoint(x: 0, y: 0), options: [])
ctx.restoreGState()
// Right gradient
ctx.saveGState()
let pathRight = UIBezierPath()
pathRight.move(to: tr)
pathRight.addLine(to: br)
pathRight.addLine(to: center)
pathRight.close()
pathRight.addClip()
ctx.drawLinearGradient(gradient, start: CGPoint(x: bounds.width, y: bounds.height), end: CGPoint(x: bounds.width, y: 0), options: [])
ctx.restoreGState()
// Bottom gradient
ctx.saveGState()
let pathBottom = UIBezierPath()
pathBottom.move(to: br)
pathBottom.addLine(to: bl)
pathBottom.addLine(to: center)
pathBottom.close()
pathBottom.addClip()
ctx.drawLinearGradient(gradient, start: CGPoint(x: 0, y: bounds.height), end: CGPoint(x: bounds.width, y: bounds.height), options: [])
ctx.restoreGState()
// Left gradient
ctx.saveGState()
let pathLeft = UIBezierPath()
pathLeft.move(to: tl)
pathLeft.addLine(to: bl)
pathLeft.addLine(to: center)
pathLeft.close()
pathLeft.addClip()
ctx.drawLinearGradient(gradient, start: CGPoint(x: 0, y: 0), end: CGPoint(x: 0, y: bounds.height), options: [])
ctx.restoreGState()
ctx.restoreGState()
}
}
let grView = FourGradientsView(frame: CGRect(x: 0, y: 0, width: 320, height: 320))
grView.backgroundColor = .black
You wrote code that always uses the same start and end color, always uses the same color locations, and always uses the same start and end points. Of course the gradients have the same gradient with the same direction.
Give your views gradient start point and end point properties as well as start and end colors. Set the gradient start points for your gradient views in your view controller's layoutDidChange() method, based on the bounds of the views. (That way you handle device rotation and different sized devices correctly.
Here's an example that you can run directly in the playground.
As you want four gradients and as gradients are draw using clipping, the graphics context is saved and restored several times (to reset the clipping).
The grading start and end point is one of the clipping corners. That's not needed. You can (and probably) should use separate points. To achieve the desired result, you sometimes want to use a start or end point considerably outside the the clipping area.
import UIKit
import PlaygroundSupport
class TriangleView2: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
let colors = [UIColor(red: 50/255.0, green: 242/255.0, blue: 111/255.0, alpha: 1).cgColor,
UIColor(red: 29/255.0, green: 127/255.0, blue: 60/255.0, alpha: 1).cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colorLocations:[CGFloat] = [0.0, 1.0]
let gradient = CGGradient(colorsSpace: colorSpace,
colors: colors as CFArray,
locations: colorLocations)!
let options: CGGradientDrawingOptions = [CGGradientDrawingOptions.drawsBeforeStartLocation, CGGradientDrawingOptions.drawsAfterEndLocation]
let p1 = CGPoint(x: 0, y: 0)
let p2 = CGPoint(x: bounds.width, y: 0)
let p3 = CGPoint(x: bounds.width, y: 20)
let p4 = CGPoint(x: bounds.width / 3, y: 140)
let p5 = CGPoint(x: 0, y: 200)
let p6 = CGPoint(x: bounds.width * 5 / 8, y: 260)
let p7 = CGPoint(x: 0, y: 230)
let p8 = CGPoint(x: bounds.width, y: 280)
let context = UIGraphicsGetCurrentContext()!
context.saveGState()
let path1 = UIBezierPath()
path1.move(to: p1)
path1.addLine(to: p2)
path1.addLine(to: p3)
path1.addLine(to: p4)
path1.close()
path1.addClip()
context.drawLinearGradient(gradient, start: p3, end: p1, options: options)
context.restoreGState()
context.saveGState()
let path2 = UIBezierPath()
path2.move(to: p1)
path2.addLine(to: p4)
path2.addLine(to: p5)
path2.close()
path2.addClip()
context.drawLinearGradient(gradient, start: p1, end: p5, options: options)
context.restoreGState()
context.saveGState()
let path3 = UIBezierPath()
path3.move(to: p3)
path3.addLine(to: p8)
path3.addLine(to: p6)
path3.addLine(to: p4)
path3.close()
path3.addClip()
context.drawLinearGradient(gradient, start: p8, end: p3, options: options)
context.restoreGState()
context.saveGState()
let path4 = UIBezierPath()
path4.move(to: p5)
path4.addLine(to: p4)
path4.addLine(to: p6)
path4.addLine(to: p7)
path4.close()
path4.addClip()
context.drawLinearGradient(gradient, start: p7, end: p6, options: options)
context.restoreGState()
}
}
let main = TriangleView2(frame: CGRect(x: 0, y: 0, width: 320, height: 500))
PlaygroundPage.current.liveView = main
Update
One more thing: Do not use the rect parameter to derive the geometry of your shapes. rect does not refer to the view size or position. Instead, it's area that needs to be redrawn. If iOS decides only part of your view needs to be redrawn, your code will draw the wrong shape.

How to draw CATransform3D for this layer?

I'm using CATransform3D and CAShapeLayer to create a layer like below
Here is my code.
let path = CGMutablePath()
let startPoint = CGPoint(x: center.x - width / 2, y: center.y - height / 2)
path.move(to: startPoint)
path.addLine(to: CGPoint(x: startPoint.x + width, y: startPoint.y))
path.addLine(to: CGPoint(x: startPoint.x + width, y: startPoint.y + height))
path.addLine(to: CGPoint(x: startPoint.x, y: startPoint.y + height))
path.closeSubpath()
let backgroundLayer = CAShapeLayer()
backgroundLayer.path = path
backgroundLayer.fillColor = UIColor.clear.cgColor
backgroundLayer.strokeColor = boarderColor.cgColor
var transform = CATransform3DIdentity
transform.m34 = -1 / 500
let angle = 45.toRadians
backgroundLayer.transform = CATransform3DRotate(transform, angle, 1, 0, 0)
The output is like below.
What is the reason for the difference of shape?
The backgroundLayer needs a frame and a position. If these are added, the result is as follows:
Source
Here a slightly modified version of your code that gives the result shown in the screenshot.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let boarderColor = UIColor.red
let height: CGFloat = 400
let width: CGFloat = 250
let center = CGPoint(x: width / 2.0, y: height / 2)
let path = CGMutablePath()
let startPoint = CGPoint(x: center.x - width / 2, y: center.y - height / 2)
path.move(to: startPoint)
path.addLine(to: CGPoint(x: startPoint.x + width, y: startPoint.y))
path.addLine(to: CGPoint(x: startPoint.x + width, y: startPoint.y + height))
path.addLine(to: CGPoint(x: startPoint.x, y: startPoint.y + height))
path.closeSubpath()
let backgroundLayer = CAShapeLayer()
backgroundLayer.path = path
backgroundLayer.fillColor = UIColor.clear.cgColor
backgroundLayer.strokeColor = boarderColor.cgColor
//these two lines are missing
backgroundLayer.frame = CGRect(x: 0, y: 0, width: width, height: height)
backgroundLayer.position = CGPoint(x: self.view.bounds.width / 2.0, y: self.view.bounds.height / 2)
var transform = CATransform3DIdentity
transform.m34 = -1 / 500
let angle = CGFloat(45 * Double.pi / 180.0)
backgroundLayer.transform = CATransform3DRotate(transform, angle, 1, 0, 0)
self.view.layer.addSublayer(backgroundLayer)
}
}

Resources