1

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.

Current Output

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...
}
M. Schwarze
  • 59
  • 1
  • 5
  • Could you draw an example of how your pie menu with 3 items should look like (including Item1, Item2, Item3 and all the lines in between) ? – olha Oct 14 '20 at 08:00
  • https://stackoverflow.com/questions/64333630/xcode-how-to-add-a-catextlayer-inside-a-circular-cashapelayer-pie-menu – M. Schwarze Oct 14 '20 at 08:25
  • 1
    @Olha the first picture shows my idea. So I want to add for every pie slice a CATextLayer inside the specific pie slice – M. Schwarze Oct 14 '20 at 08:26
  • Are you trying to do something like this? https://stackoverflow.com/a/62269780/6257435 – DonMag Oct 14 '20 at 13:10

1 Answers1

2

So, here's a rough prototype which does the stuff you need, but not very precise. enter image description here

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

olha
  • 2,132
  • 1
  • 18
  • 39
  • I did it a little bit different (I calculated the position inside the whole frame for every text item) but thanks a lot for your answer :D – M. Schwarze Oct 14 '20 at 13:45