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)
}
}
}