My View Controller code contains below code
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let width: CGFloat = 240
let height: CGFloat = 240
let demoView = DemoView(frame: CGRect(x: self.view.frame.size.width/2 - width/2,
y: self.view.frame.size.height/2 - height/2,
width: width,
height: height))
let subView = UIView.init(frame: (CGRect(x: demoView.frame.origin.x - width,
y: demoView.frame.origin.y,
width: width * 2,
height: height * 2)))
self.view.addSubview(demoView)
self.view.addSubview(subView)
subView.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5)
subView.layer.cornerRadius = subView.frame.size.height / 2
}
}
import UIKit
class DemoView: UIView {
var path: UIBezierPath!
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.darkGray
}
override func draw(_ rect: CGRect) {
self.createTriangle()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func createTriangle() {
let count : Int = 9
let gap : CGFloat = 3
let yValue : CGFloat = CGFloat(self.frame.size.width - ((CGFloat(count - 1)) * gap)) / CGFloat(count);
for a in 0 ..< count {
let i : CGFloat = CGFloat(a)
let path1: UIBezierPath! = UIBezierPath()
path1.move(to: CGPoint(x: 0.0, y: self.frame.size.height))
path1.addLine(to: CGPoint(x: (yValue * i) > 0 ? (yValue * i) + i*gap : 0, y: (yValue * i) > 0 ? (yValue * i) + i*gap : 0))
path1.addLine(to: CGPoint(x:yValue * (i+1) + i*gap, y: yValue * (i+1) + i*gap))
path1.close()
UIColor.orange.setFill()
path1.fill()
}
}
}
Can anyone help me to achieve this thing?
Edit image :
Is this the result your want ?
The way I did it isn't with triangles but Arcs.
Add createPie() in your DemoView class and call it in draw(:) instead of your createTriangle().
This is my code:
func createPie() {
// 2 vars to configure width of gap/banches
var branchAmount = 10
var gapAngle = CGFloat.pi / 100
let startAngle = 3 * CGFloat.pi / 2
let endAngle = 2 * CGFloat.pi
let branchAngle = (endAngle - startAngle - (CGFloat(branchAmount) - 1) * gapAngle) / CGFloat(branchAmount)
let paths = UIBezierPath()
for i in 0..<branchAmount {
paths.move(to: CGPoint(x: 0.0, y: self.frame.size.height))
paths.addArc(withCenter: CGPoint(x: 0, y: self.frame.size.height),
radius: self.frame.size.height,
startAngle: startAngle + CGFloat(i) * (branchAngle + gapAngle),
endAngle: startAngle + CGFloat(i) * (branchAngle + gapAngle) + branchAngle,
clockwise: true)
}
paths.close()
UIColor.orange.setFill()
paths.fill()
}
Cheers!
EDIT: If you want to add a circular mask you and add this in the end of createPie() (which is no longer really a pie now..):
// Circular mask
let maskLayer = CAShapeLayer()
let maskPath = UIBezierPath(rect: bounds)
maskLayer.fillRule = kCAFillRuleEvenOdd // Circle will be substracted to the mask thanks to this
maskPath.move(to: CGPoint(x: 0.0, y: frame.size.height))
maskPath.addArc(withCenter: CGPoint(x: 0, y: frame.size.height), radius: maskRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
maskLayer.path = maskPath.cgPath
layer.mask = maskLayer
It just adds mask composed of the subtraction of bounds and the circle of origin (0, height)
Related
I'm fairly new to ios programming
I try to draw some arcs for my circular progress bar in UIView.draw(rect:) method.
I'm using UIGraphicsGetCurrentContext to get the context and add arc path and draw in the method of custom UIView called MyProgressView
So I made an object of my custom view and it worked fine
But if I make a couple of more objects, they don't work
UIView.draw(rect:) is called every time whenever it needs to be.
the first one is drawing properly and the rest are not
class MyProgressView: UIView {
var startAngle: Double = 0.0
var endAngle: Double = 0.0 {
didSet{
self.setNeedsDisplay()
}
}
func radian(_ degree: Double) -> Double {
return (degree - 90) * Double.pi / 180.0
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.setLineWidth(10)
context.setStrokeColor(UIColor.white.cgColor)
context.addArc(center: self.center, radius: 30, startAngle: radian(startAngle), endAngle: radian(endAngle), clockwise: false)
context.strokePath()
}
}
I put self.setNeedsDisplay() in didSet of a variable called endAngle
and inside of view controller's viewDidLoad,
let progressBar = MyProgressView()
progressBar.frame = CGRect(x: 0, y: 0, width: view.frame.width / 2, height:view.frame.height / 2)
progressBar.backgroundColor = .systemMint
progressBar.startAngle = 0
progressBar.endAngle = 180
view.addSubview(progressBar)
and it looks fine
but if I add two more, they don't work
let progressBar2 = MyProgressView()
progressBar2.frame = CGRect(x: 150, y: 0, width: view.frame.width / 2, height: view.frame.height / 2)
progressBar2.backgroundColor = .systemPink
progressBar2.startAngle = 90
progressBar2.endAngle = 270
view.addSubview(progressBar2)
let progressBar3 = MyProgressView()
progressBar3.frame = CGRect(x: 0, y: 150, width: view.frame.width / 2, height: view.frame.height / 2)
progressBar3.backgroundColor = .systemIndigo
progressBar3.startAngle = 90
progressBar3.endAngle = 270
view.addSubview(progressBar3)
Can anybody explain what's missing here?
I made it and run in playground, iOS version is 15.4
Thanks
You have 2 problems
1- The method that converts to radians isn't correct
2- You need to use a zero based center point
class MyProgressView: UIView {
var startAngle: Double = 0.0
var endAngle: Double = 0.0
func radian(_ number: Double) -> CGFloat {
return CGFloat(number * .pi / 180)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.setLineWidth(10)
context.setStrokeColor(UIColor.white.cgColor)
context.addArc(center: CGPoint(x:rect.size.width / 2, y:rect.size.height / 2), radius: 30, startAngle: radian(startAngle), endAngle: radian(endAngle), clockwise: false)
context.strokePath()
}
}
Full demo
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let progressBar = MyProgressView()
progressBar.frame = CGRect(x: 0, y: 0, width: view.frame.width / 2, height:view.frame.height / 2)
progressBar.backgroundColor = .systemMint
progressBar.startAngle = 0
progressBar.endAngle = 180
view.addSubview(progressBar)
let progressBar2 = MyProgressView()
progressBar2.frame = CGRect(x: 150, y: 0, width: view.frame.width / 2, height: view.frame.height / 2)
progressBar2.backgroundColor = .systemPink
progressBar2.startAngle = 90
progressBar2.endAngle = 270
view.addSubview(progressBar2)
let progressBar3 = MyProgressView()
progressBar3.frame = CGRect(x: 0, y: 300, width: view.frame.width / 2, height: view.frame.height / 2)
progressBar3.backgroundColor = .systemIndigo
progressBar3.startAngle = 90
progressBar3.endAngle = 270
view.addSubview(progressBar3)
}
}
class MyProgressView: UIView {
var startAngle: Double = 0.0
var endAngle: Double = 0.0
func radian(_ number: Double) -> CGFloat {
return CGFloat(number * .pi / 180)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.setLineWidth(10)
context.setStrokeColor(UIColor.white.cgColor)
context.addArc(center: CGPoint(x:rect.size.width / 2, y:rect.size.height / 2), radius: 30, startAngle: radian(startAngle), endAngle: radian(endAngle), clockwise: false)
context.strokePath()
}
}
I simply do the following in code:
let path = UIBezierPath(rect: blurView.bounds)
path.usesEvenOddFillRule = true
path.append(UIBezierPath(rect: CGRect(x: 100, y: 100, width: 100, height: 100)))
path.append(UIBezierPath(rect: CGRect(x: 150, y: 150, width: 100, height: 100)))
//here you can add more paths, but the number is not known
let layer = CAShapeLayer()
layer.path = path.cgPath
layer.fillRule = .evenOdd
blurView.layer.mask = layer
and the effect is following:
Two rectangles overlapping one another. But all I need is to combine area from both rectanges, not to exclude everlapping area. Is it possible?
Using the "even-odd" fill rule is great for "cutting a hole" in a path. However, this code:
// create a big rect
let path = UIBezierPath(rect: blurView.bounds)
// cut a hole in it
path.append(UIBezierPath(rect: CGRect(x: 100, y: 100, width: 100, height: 100)))
// cut a hole overlapping a hole?
path.append(UIBezierPath(rect: CGRect(x: 150, y: 150, width: 100, height: 100)))
will be, as you've seen, problematic.
Depending on what all you are wanting to do, you could use a library such as ClippingBezier which allows you to manipulate paths with boolean actions.
Or, you can use a custom CALayer like this to "invert" multiple paths to use as a "cutout mask":
class BasicCutoutLayer: CALayer {
var rects: [CGRect] = []
func addRect(_ newRect: CGRect) {
rects.append(newRect)
setNeedsDisplay()
}
func reset() {
rects = []
setNeedsDisplay()
}
override func draw(in ctx: CGContext) {
// fill entire layer with solid color
ctx.setFillColor(UIColor.gray.cgColor)
ctx.fill(self.bounds);
rects.forEach { r in
ctx.addPath(UIBezierPath(rect: r).cgPath)
}
// draw clear "cutouts"
ctx.setFillColor(UIColor.clear.cgColor)
ctx.setBlendMode(.sourceIn)
ctx.drawPath(using: .fill)
}
}
To show it in use, we'll use this image:
In a standard UIImageView, overlaid with a blur UIVisualEffectView, and then use the BasicCutoutLayer class with two overlapping rects as the blur view's layer mask:
class BasicCutoutVC: UIViewController {
let myBlurView = UIVisualEffectView()
let myCutoutLayer = BasicCutoutLayer()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
let imgView = UIImageView()
if let img = UIImage(named: "sampleBG") {
imgView.image = img
}
[imgView, myBlurView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
imgView.topAnchor.constraint(equalTo: g.topAnchor),
imgView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
imgView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
imgView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
myBlurView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
myBlurView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
myBlurView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
myBlurView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
myBlurView.effect = UIBlurEffect(style: .extraLight)
// set mask for blur view
myBlurView.layer.mask = myCutoutLayer
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// set mask layer frame
myCutoutLayer.frame = myBlurView.bounds
// add two overlapping rects
let v: CGFloat = 160
let c: CGPoint = CGPoint(x: myBlurView.bounds.midX, y: myBlurView.bounds.midY)
var r: CGRect = CGRect(origin: c, size: CGSize(width: v, height: v))
r.origin.x -= v * 0.75
r.origin.y -= v * 0.75
myCutoutLayer.addRect(r)
r.origin.x += v * 0.5
r.origin.y += v * 0.5
myCutoutLayer.addRect(r)
}
}
Before applying the mask, it looks like this:
after applying the mask we get:
As we see, the "overlap" displays as we want.
That was a very simple, basic example. For a more advanced example, take a look at this:
struct MyPath {
var lineWidth: CGFloat = 0
var lineCap: CGLineCap = .butt
var lineJoin: CGLineJoin = .bevel
var isStroked: Bool = true
var isFilled: Bool = true
var pth: UIBezierPath = UIBezierPath()
}
class AdvancedCutoutLayer: CALayer {
var myPaths: [MyPath] = []
func addPath(_ newPath: MyPath) {
myPaths.append(newPath)
setNeedsDisplay()
}
func reset() {
myPaths = []
setNeedsDisplay()
}
override func draw(in ctx: CGContext) {
// fill entire layer with solid color
ctx.setFillColor(UIColor.gray.cgColor)
ctx.fill(self.bounds);
ctx.setBlendMode(.sourceIn)
myPaths.forEach { thisPath in
ctx.setStrokeColor(thisPath.isStroked ? UIColor.clear.cgColor : UIColor.black.cgColor)
ctx.setFillColor(thisPath.isFilled ? UIColor.clear.cgColor : UIColor.black.cgColor)
ctx.setLineWidth(thisPath.isStroked ? thisPath.lineWidth : 0.0)
ctx.setLineCap(thisPath.lineCap)
ctx.setLineJoin(thisPath.lineJoin)
ctx.addPath(thisPath.pth.cgPath)
ctx.drawPath(using: .fillStroke)
}
}
}
along with a subclassed UIVisualEffectView for convenience:
class CutoutBlurView: UIVisualEffectView {
let sl = AdvancedCutoutLayer()
override init(effect: UIVisualEffect?) {
super.init(effect: effect)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
sl.isOpaque = false
layer.mask = sl
}
override func layoutSubviews() {
super.layoutSubviews()
sl.frame = bounds
sl.setNeedsDisplay()
}
func addPath(_ newPath: MyPath) {
sl.addPath(newPath)
}
func reset() {
sl.reset()
}
}
and an example controller:
class AdvancedCutoutVC: UIViewController {
let myView = CutoutBlurView()
var idx: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
let imgView = UIImageView()
if let img = UIImage(named: "sampleBG") {
imgView.image = img
}
[imgView, myView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
imgView.topAnchor.constraint(equalTo: g.topAnchor),
imgView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
imgView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
imgView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
myView.topAnchor.constraint(equalTo: g.topAnchor),
myView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
myView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
myView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
myView.effect = UIBlurEffect(style: .extraLight)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true, block: { _ in
switch self.idx % 4 {
case 1:
self.addSomeOvals()
case 2:
self.addSomeLines()
case 3:
self.addSomeShapes()
default:
self.addSomeRects()
}
self.idx += 1
})
}
func addSomeRects() {
myView.reset()
let w: CGFloat = myView.frame.width / 4.0
let h: CGFloat = myView.frame.height / 4.0
var x: CGFloat = ((myView.frame.width - (w * 5.0 * 0.5)) * 0.5) - (w * 0.25)
var y: CGFloat = ((myView.frame.height - (h * 5.0 * 0.5)) * 0.5) - (h * 0.25)
for _ in 1...5 {
let bz = UIBezierPath(rect: CGRect(x: x, y: y, width: w, height: h))
myView.addPath(MyPath(lineWidth: 0, isStroked: false, isFilled: true, pth: bz))
x += w * 0.5
y += h * 0.5
}
}
func addSomeOvals() {
myView.reset()
let w: CGFloat = myView.frame.width / 4.0
let h: CGFloat = myView.frame.height / 4.0
var x: CGFloat = ((myView.frame.width - (w * 5.0 * 0.5)) * 0.5) - (w * 0.25)
var y: CGFloat = ((myView.frame.height - (h * 5.0 * 0.5)) * 0.5) - (h * 0.25)
for _ in 1...5 {
let bz = UIBezierPath(ovalIn: CGRect(x: x, y: y, width: w, height: h))
myView.addPath(MyPath(lineWidth: 0, isStroked: false, isFilled: true, pth: bz))
x += w * 0.5
y += h * 0.5
}
}
func addSomeLines() {
myView.reset()
let w: CGFloat = myView.frame.width / 2.0
let h: CGFloat = myView.frame.height / 4.0
let x: CGFloat = 80
var y: CGFloat = 80
var lw: CGFloat = 4
for _ in 1...5 {
let bz = UIBezierPath()
bz.move(to: CGPoint(x: x, y: y))
bz.addLine(to: CGPoint(x: x + w, y: y + 20))
myView.addPath(MyPath(lineWidth: lw, lineCap: .round, isStroked: true, isFilled: false, pth: bz))
y += h * 0.5
lw += 10
}
}
func addSomeShapes() {
myView.reset()
var bz: UIBezierPath!
bz = UIBezierPath(rect: CGRect(x: 80, y: 80, width: 80, height: 120))
myView.addPath(MyPath(isStroked: false, isFilled: true, pth: bz))
bz = UIBezierPath(rect: CGRect(x: 120, y: 120, width: 120, height: 60))
myView.addPath(MyPath(isStroked: false, isFilled: true, pth: bz))
bz = UIBezierPath(rect: CGRect(x: 80, y: 220, width: 220, height: 60))
myView.addPath(MyPath(lineWidth: 12, isStroked: true, isFilled: false, pth: bz))
bz = UIBezierPath(ovalIn: CGRect(x: 100, y: 240, width: 220, height: 60))
myView.addPath(MyPath(lineWidth: 12, isStroked: true, isFilled: false, pth: bz))
var r: CGRect = CGRect(x: 40, y: 320, width: myView.frame.width - 80, height: 200)
for _ in 1...4 {
bz = UIBezierPath(rect: r)
myView.addPath(MyPath(lineWidth: 8, isStroked: true, isFilled: false, pth: bz))
r = r.insetBy(dx: 20, dy: 20)
}
}
}
When run, this example will cycle through overlapping rect, overlapping ovals, some varying width lines, and some assorted shapes (just to give an idea):
I would go with ClippingBezier because it is fast, easy to use and neat. It'll be something like this:
let rect1 = CGRect(x: 100, y: 100, width: 200, height: 200)
let rect2 = CGRect(x: 150, y: 200, width: 200, height: 200)
let path0 = UIBezierPath(rect: blurView.bounds)
let path1 = UIBezierPath(rect: rect1)
let path2 = UIBezierPath(rect: rect2)
let unionPathArray = path1.union(with: path2)
let unionPath = UIBezierPath()
if let array = unionPathArray {
array.forEach(unionPath.append)
path0.append(unionPath.reversing())
let layerUnion = CAShapeLayer()
layerUnion.path = path0.cgPath
blurView.layer.mask = layerUnion
}
Output:
EDIT
It appears that this method doesn't work properly when using UIBezierPath(roundedRect:cornerRadius:). To overcome that, here is how we can construct our own func to do that:
extension UIBezierPath {
convenience init(rectangleIn rect: CGRect, cornerRadius: CGFloat) {
self.init()
move(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius))
addArc(withCenter: CGPoint(x: rect.minX + cornerRadius, y: rect.minY + cornerRadius), radius: cornerRadius, startAngle: .pi, endAngle: 3.0 * .pi / 2.0, clockwise: true)
addLine(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY))
addArc(withCenter: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY + cornerRadius), radius: cornerRadius, startAngle: 3.0 * .pi / 2.0, endAngle: 2 * .pi, clockwise: true)
addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerRadius))
addArc(withCenter: CGPoint(x: rect.maxX - cornerRadius, y: rect.maxY - cornerRadius), radius: cornerRadius, startAngle: 0.0, endAngle: .pi / 2.0, clockwise: true)
addLine(to: CGPoint(x: rect.minX + cornerRadius, y: rect.maxY))
addArc(withCenter: CGPoint(x: rect.minX + cornerRadius, y: rect.maxY - cornerRadius), radius: cornerRadius, startAngle: .pi / 2.0, endAngle: .pi, clockwise: true)
//addLine(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius))
close()
}
}
We can also extend the above-mentioned solution to multiple paths. Here is one way to create the union of multiple paths:
extension UIBezierPath {
class func getUnion(of paths: [UIBezierPath]) -> UIBezierPath {
var result = UIBezierPath()
paths.forEach { subPath in
guard let union = result.union(with: subPath) else { return }
let unionCombined = UIBezierPath()
union.forEach(unionCombined.append)
result = unionCombined
}
return result
}
}
Here is an example:
let rect1 = CGRect(x: 100, y: 100, width: 200, height: 180)
let rect2 = CGRect(x: 150, y: 200, width: 200, height: 200)
let rect3 = CGRect(x: 150, y: 500, width: 100, height: 100)
let rect4 = CGRect(x: 150, y: 800, width: 300, height: 100)
let pathBase = UIBezierPath(rect: blurView.bounds)
let path1 = UIBezierPath(rectangleIn: rect1, cornerRadius: 20.0)
let path2 = UIBezierPath(rect: rect2)
let path3 = UIBezierPath(ovalIn: rect3)
let path4 = UIBezierPath(ovalIn: rect4)
let union = UIBezierPath.getUnion(of: [path1, path2, path3, path4])
pathBase.append(union.reversing())
let layerUnion = CAShapeLayer()
layerUnion.path = pathBase.cgPath
blurView.layer.mask = layerUnion
And the output:
I want to program a custom pie menu. In the code below you see how I create a pie menu with two items. My structure is the following: I'm using a rectengular UIBezierPath with a CAShapeLayer as the context as my circular background. Inside my circular background I've got a child, the inner small circle (also UIBezierPath with CAShapeLayer). The other childs of my circular background layer are the items, which are also a CAShapeLayer with using a custom UIBezierPath (I draw my items depends on the number of items (different degrees and so on)). Now I want to add inside every item layer a CATextLayer ("Item 1", "Item 2" and so on). My problem is, that I don't know how to set the frame of my specific item layers and how I can add the specific CATextLayer in the way that the text is dynamically inside the parent item layer. In my case the CATextLayer depends on the frame of the menu background layer.
func setMenuBackgroundLayer() {
//Draw a circle background with UIBezierPath for the static pie menu
let path = UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width / 2, y: self.frame.size.height / 2), radius: menuRadius, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi * 2), clockwise: true)
menuBackgroundLayer = CAShapeLayer()
menuBackgroundLayer.path = path.cgPath
menuBackgroundLayer.fillColor = menuBackgroundLayerColor.cgColor
menuBackgroundLayer.frame = self.bounds
menuBackgroundLayer.zPosition = 1
self.layer.addSublayer(menuBackgroundLayer)
//Draw the inner circle (back button)
let pathInner = UIBezierPath(arcCenter: CGPoint(x: menuBackgroundLayer.frame.size.width / 2, y: menuBackgroundLayer.frame.size.height / 2), radius: innerCircleRadius, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi * 2), clockwise: true)
innerCircleLayer = CAShapeLayer()
innerCircleLayer.path = pathInner.cgPath
innerCircleLayer.fillColor = menuBackgroundLayerColor.cgColor
innerCircleLayer.strokeColor = UIColor.black.cgColor
innerCircleLayer.lineWidth = 1
innerCircleLayer.frame = menuBackgroundLayer.frame
menuBackgroundLayer.addSublayer(innerCircleLayer)
//Set the inner circle above all other menu items
innerCircleLayer.zPosition = 100
//Add the arrow image inside the inner circle
//addBackImage()
}
func insertMenuItems() {
//Compare which item has to get inserted and insert it
if numberOfItems == 1 {
let path = UIBezierPath(arcCenter: CGPoint(x: menuBackgroundLayer.frame.size.width / 2, y: menuBackgroundLayer.frame.size.height / 2), radius: menuRadius, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi * 2), clockwise: true)
item1Layer = CAShapeLayer()
item1Layer.path = path.cgPath
item1Layer.fillColor = menuBackgroundLayerColor.cgColor
item1Layer.strokeColor = UIColor.black.cgColor
item1Layer.lineWidth = 1
item1Layer.frame = menuBackgroundLayer.bounds
menuBackgroundLayer.addSublayer(item1Layer)
item1Layer.zPosition = 2
let textLayer = CATextLayer()
textLayer.string = "ITEM 1"
textLayer.foregroundColor = UIColor.white.cgColor
textLayer.font = UIFont(name: "Avenir", size: 15.0)
textLayer.fontSize = 15.0
textLayer.alignmentMode = CATextLayerAlignmentMode.center
textLayer.zPosition = 3
textLayer.frame = item1Layer.bounds
textLayer.position = CGPoint(x: item1Layer.position.x, y: item1Layer.position.y + 20.0)
textLayer.contentsScale = UIScreen.main.scale
item1Layer.addSublayer(textLayer)
}
else if numberOfItems == 2 {
//Item 1
let path1 = UIBezierPath()
path1.move(to: CGPoint(x: menuBackgroundLayer.frame.size.width / 2, y: menuBackgroundLayer.frame.size.height / 2))
path1.addArc(withCenter: CGPoint(x: menuBackgroundLayer.frame.size.width / 2, y: menuBackgroundLayer.frame.size.height / 2), radius: menuRadius, startAngle: rad2deg(180.0), endAngle: rad2deg(0.0), clockwise: true)
path1.close()
item1Layer = CAShapeLayer()
item1Layer.path = path1.cgPath
item1Layer.fillColor = menuBackgroundLayerColor.cgColor
item1Layer.strokeColor = UIColor.black.cgColor
item1Layer.lineWidth = 1
item1Layer.frame = menuBackgroundLayer.bounds
menuBackgroundLayer.addSublayer(item1Layer)
item1Layer.zPosition = 2
let textLayer1 = CATextLayer()
textLayer1.string = "ITEM 1"
textLayer1.foregroundColor = UIColor.white.cgColor
textLayer1.font = UIFont(name: "Avenir", size: 15.0)
textLayer1.fontSize = 15.0
textLayer1.alignmentMode = CATextLayerAlignmentMode.center
textLayer1.zPosition = 3
textLayer1.frame = item1Layer.bounds
textLayer1.position = CGPoint(x: item1Layer.position.x, y: item1Layer.position.y + 20.0)
textLayer1.contentsScale = UIScreen.main.scale
item1Layer.addSublayer(textLayer1)
//Item 2
let path2 = UIBezierPath()
path2.move(to: CGPoint(x: menuBackgroundLayer.frame.size.width / 2, y: menuBackgroundLayer.frame.size.height / 2))
path2.addArc(withCenter: CGPoint(x: menuBackgroundLayer.frame.size.width / 2, y: menuBackgroundLayer.frame.size.height / 2), radius: menuRadius, startAngle: rad2deg(0.0), endAngle: rad2deg(180.0), clockwise: true)
path2.close()
item2Layer = CAShapeLayer()
item2Layer.path = path2.cgPath
item2Layer.fillColor = menuBackgroundLayerColor.cgColor
item2Layer.strokeColor = UIColor.black.cgColor
item2Layer.lineWidth = 1
item2Layer.frame = menuBackgroundLayer.bounds
menuBackgroundLayer.addSublayer(item2Layer)
item2Layer.zPosition = 2
let textLayer2 = CATextLayer()
textLayer2.string = "ITEM 2"
textLayer2.foregroundColor = UIColor.white.cgColor
textLayer2.font = UIFont(name: "Avenir", size: 15.0)
textLayer2.fontSize = 15.0
textLayer2.alignmentMode = CATextLayerAlignmentMode.center
textLayer2.zPosition = 3
textLayer2.frame = item2Layer.bounds
textLayer2.position = CGPoint(x: item2Layer.position.x, y: item2Layer.position.y + 20.0)
textLayer2.contentsScale = UIScreen.main.scale
item2Layer.addSublayer(textLayer2)
}
and so on...
}
So, here's a rough prototype which does the stuff you need, but not very precise.
If you want to rotate the text, this can be achieved with CATransform.
You can play with the code here: https://github.com/gatamar/stackoverflow_answers/tree/master/so64348954
Or I can make it more precise, if this is almost what you need.
The code for Pie Menu:
import Foundation
import UIKit
class HackLinesView: UIView {
init(frame: CGRect, partsCount parts: Int) {
super.init(frame: frame)
backgroundColor = .clear
let side = frame.width/2
// add lines
for part in 0..<parts {
let angle = CGFloat(part)/CGFloat(parts) * 2 * .pi
let lineLayer = CAShapeLayer()
lineLayer.backgroundColor = UIColor.black.cgColor
let path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 1, height: side))
lineLayer.path = path.cgPath
lineLayer.transform = CATransform3DMakeRotation(angle, 0, 0, 1)
layer.addSublayer(lineLayer)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class PieMenuView: UIView {
init(frame: CGRect, partsCount parts: Int) {
assert( abs(frame.width-frame.height) < 0.001)
super.init(frame: frame)
setupLayers(parts)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupLayers(_ parts: Int) {
let side = bounds.width
let outerRadius = side * 0.5
let innerRadius = side * 0.2
// add outer circle
let outerCircleLayer = CAShapeLayer()
outerCircleLayer.frame = bounds
outerCircleLayer.cornerRadius = outerRadius
outerCircleLayer.backgroundColor = UIColor.orange.cgColor
layer.addSublayer(outerCircleLayer)
// add inner circle
let innerCircleLayer = CAShapeLayer()
innerCircleLayer.frame = CGRect(x: side/2-innerRadius, y: side/2-innerRadius, width: innerRadius*2, height: innerRadius*2)
innerCircleLayer.cornerRadius = innerRadius
innerCircleLayer.backgroundColor = UIColor.yellow.cgColor
layer.addSublayer(innerCircleLayer)
let linesView = HackLinesView(frame: CGRect(x: side/2, y: side/2, width: side, height: side), partsCount: parts)
addSubview(linesView)
// add text
for part in 0..<parts {
let angle = CGFloat(part)/CGFloat(parts) * 2 * .pi
let textLayer = CATextLayer()
textLayer.string = String(format: "%d", part)
textLayer.foregroundColor = UIColor.blue.cgColor
// calc the center for text layer
let x1 = side/2
let y1 = side/2
let x2 = x1 + cos(angle)*outerRadius
let y2 = y1 + sin(angle)*outerRadius
let textCenterX = (x1 + x2)/2, textCenterY = (y1 + y2)/2
let textLayerSide: CGFloat = 50
textLayer.frame = CGRect(x: textCenterX-textLayerSide/2, y: textCenterY-textLayerSide/2, width: textLayerSide, height: textLayerSide)
layer.addSublayer(textLayer)
}
}
}
I want to crop an UIView with bottom and top of repeated semi circle like this image
I had been working on your question and here is my results, you need create a UIBezierPath and apply to your desired view, use this code for that
Function to generate the desired BezierPath
func pathSemiCirclesPathForView(givenView: UIView, ciclesRadius:CGFloat = 10, circlesDistance : CGFloat = 2) ->UIBezierPath
{
let width = givenView.frame.size.width
let height = givenView.frame.size.height
let semiCircleWidth = CGFloat(ciclesRadius*2)
let semiCirclesPath = UIBezierPath()
semiCirclesPath.move(to: CGPoint(x:0, y:0))
var x = CGFloat(0)
var i = 0
while x < width {
x = (semiCircleWidth) * CGFloat(i) + (circlesDistance * CGFloat(i))
let pivotPoint = CGPoint(x: x + semiCircleWidth/2, y: height)
semiCirclesPath.addArc(withCenter: pivotPoint, radius: ciclesRadius, startAngle: -180 * .pi / 180.0, endAngle: 0 * .pi / 180.0, clockwise: true)
semiCirclesPath.addLine(to: CGPoint(x: semiCirclesPath.currentPoint.x + circlesDistance, y: height))
i += 1
}
semiCirclesPath.addLine(to: CGPoint(x:width,y: 0))
i = 0
while x > 0 {
x = width - (semiCircleWidth) * CGFloat(i) - (circlesDistance * CGFloat(i))
let pivotPoint = CGPoint(x: x - semiCircleWidth/2, y: 0)
semiCirclesPath.addArc(withCenter: pivotPoint, radius: ciclesRadius, startAngle: 0 * .pi / 180.0, endAngle: -180 * .pi / 180.0, clockwise: true)
semiCirclesPath.addLine(to: CGPoint(x: semiCirclesPath.currentPoint.x - circlesDistance, y: 0))
i += 1
}
semiCirclesPath.close()
return semiCirclesPath
}
Function to apply the BezierPath to any View
func applySemiCircleEffect(givenView: UIView){
let shapeLayer = CAShapeLayer(layer: givenView.layer)
shapeLayer.path = self.pathSemiCirclesPathForView(givenView: givenView).cgPath
shapeLayer.frame = givenView.bounds
shapeLayer.masksToBounds = true
shapeLayer.shadowOpacity = 1
shapeLayer.shadowColor = UIColor.black.cgColor
shapeLayer.shadowOffset = CGSize(width: 0, height: 0)
shapeLayer.shadowRadius = 3
givenView.layer.mask = shapeLayer
}
Use it
#IBOutlet weak var customView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.applySemiCircleEffect(givenView: customView)
}
This is how it looks
Hope this helps you, Happy Coding
I am trying to Implement drawing of Arc from certain start angle to certain end angle. I am trying to animate drawing of arc but have no luck.
I have looked for couple of implementations but all of them refers to be drawing of animating boder of circle.
Like drawing a pie but with one color. With certain start and end angle. Animated drawing of pie.
What I like is to have have animated filling while arc is being animated from start angle to end angle.
Like arc is being drawn clockwise and while drawing it is filling its inside color.
Any idea How can I acheive this in Swift?
Thanks in advance.
Edit
This is what I have drawn:
(gray color shows area covered by drawing pie)
Code for drawing Pie
import Foundation
import UIKit
#IBDesignable class PieChart : UIView {
#IBInspectable var percentage: CGFloat = 50 {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var outerBorderWidth: CGFloat = 2 {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var outerBorderColor: UIColor = UIColor.white {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var percentageFilledColor: UIColor = UIColor.primary {
didSet {
self.setNeedsDisplay()
}
}
let circleLayer = CAShapeLayer()
override func draw(_ rect: CGRect) {
drawPie(rect: rect, endPercent: percentage, color: UIColor.primary)
}
func drawPie(rect: CGRect, endPercent: CGFloat = 70, color: UIColor) {
let center = CGPoint(x: rect.origin.x + rect.width / 2, y: rect.origin.y + rect.height / 2)
let radius = min(rect.width, rect.height) / 2
let gpath = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: CGFloat(360.degreesToRadians), clockwise: true)
UIColor.white.setFill()
gpath.fill()
let circlePath = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(0), endAngle:CGFloat(360.degreesToRadians), clockwise: true)
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = outerBorderColor.cgColor
circleLayer.lineWidth = outerBorderWidth
circleLayer.borderColor = outerBorderColor.cgColor
self.layer.addSublayer(circleLayer)
let π: CGFloat = 3.14
let halfPi: CGFloat = π / 2
let startPercent: CGFloat = 0.0
let startAngle = (startPercent / 100 * π * 2 - π ) + halfPi
let endAngle = (endPercent / 100 * π * 2 - π ) + halfPi
let path = UIBezierPath()
path.move(to: center)
path.addArc(withCenter: center, radius: radius, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: true)
path.close()
percentageFilledColor.setFill()
path.fill()
}
}
I've created this code using playground and swift. This code will draw a pie chart animating the filling color. Make sure you have the Timeline pane open on Xcode to see the animation working. I couldn't find a way to pass the path function directly to CAAnimation so my workaround was to create a list of steps, each one containing the path for a percentage stage. You can change the steps for a smother animation.
//: Playground - noun: a place where people can play
import UIKit
import XCPlayground
let STEPS_ANIMATION = 50
let initialPercentage : CGFloat = 0.10
let finalPercentage : CGFloat = 0.75
func buildPiePath(frame : CGRect, percentage : CGFloat) -> UIBezierPath {
let newPath = UIBezierPath()
let startPoint = CGPoint(x: frame.size.width, y: frame.height / 2.0)
let centerPoint = CGPoint(x: frame.size.width / 2.0, y: frame.height / 2.0)
let startAngle : CGFloat = 0
let endAngle : CGFloat = percentage * 2 * CGFloat(M_PI)
newPath.move(to: centerPoint)
newPath.addLine(to: startPoint)
newPath.addArc(withCenter: centerPoint,
radius: frame.size.width / 2.0,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
newPath.addLine(to: centerPoint)
newPath.close()
return newPath
}
func buildPiePathList(frame: CGRect,
startPercentage: CGFloat,
finalPercentage: CGFloat) -> [AnyObject] {
var listValues = [AnyObject]()
for index in 1...STEPS_ANIMATION {
let delta = finalPercentage - startPercentage
let currentPercentage = CGFloat(index) / CGFloat(STEPS_ANIMATION)
let percentage = CGFloat(startPercentage + (delta * currentPercentage))
listValues.append(buildPiePath(frame: frame,
percentage: percentage)
.cgPath)
}
return listValues
}
// Container for pie chart
let container = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
container.backgroundColor = UIColor.gray
XCPShowView(identifier: "Container View", view: container)
let circleFrame = CGRect(x: 20, y: 20, width: 360, height: 360)
// Red background
let background = CAShapeLayer()
background.frame = circleFrame
background.fillColor = UIColor.red.cgColor
background.path = buildPiePath(frame: circleFrame, percentage: 1.0).cgPath
container.layer.addSublayer(background)
// Green foreground that animates
let foreground = CAShapeLayer()
foreground.frame = circleFrame
foreground.fillColor = UIColor.green.cgColor
foreground.path = buildPiePath(frame: circleFrame, percentage: initialPercentage).cgPath
container.layer.addSublayer(foreground)
// Filling animation
let fillAnimation = CAKeyframeAnimation(keyPath: "path")
fillAnimation.values = buildPiePathList(frame: circleFrame,
startPercentage: initialPercentage,
finalPercentage: finalPercentage)
fillAnimation.duration = 3
fillAnimation.calculationMode = kCAAnimationDiscrete
foreground.add(fillAnimation, forKey:nil)