Search code examples
iosswiftxcodecashapelayer

iOS - How to set a CATextLayer inside a CAShapeLayer which is drawn with a custom UIBezierPath?


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

Solution

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