Reputation: 2639
I have created a circle using a CAShapeLayer. Now I want to add text to the control but I am not quite sure on how to do so (so it looks good).
I have the following code:
import Foundation
import UIKit
class Gauge : UIView
{
private var shapeLayer = CAShapeLayer()
private var maskingLayer = CAShapeLayer()
private var gradientLayer = CAGradientLayer()
private var textLayers: [CATextLayer] = []
private var mValue: CGFloat = 0.0
private var mSegments = 9
private let textHeight: CGFloat = 24.0
// MARK: Properties
var lineWidth: CGFloat = 32.0
var min: CGFloat = 0.0
var max: CGFloat = 100.0
var segments: Int
{
get { return self.mSegments - 1 }
set
{
self.mSegments = newValue + 1
self.commonInit()
}
}
var progress: CGFloat
{
get
{
let diff = abs(self.min) + self.max
return self.value / diff
}
}
var segmentSize: CGFloat = 270.0
{
didSet
{
self.value = 0.0
self.commonInit()
}
}
var value: CGFloat
{
get { return self.mValue }
set
{
if self.mValue == newValue { return }
if newValue < 0.0
{
self.mValue = 0.0
}
else if newValue > self.max
{
self.mValue = self.max
}
else
{
self.mValue = newValue
}
self.maskingLayer.strokeStart = 0.0
self.maskingLayer.strokeEnd = 0.5
}
}
override init(frame: CGRect)
{
super.init(frame: frame)
self.commonInit()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
self.commonInit()
}
fileprivate func commonInit()
{
self.value = 50
self.determineLineWidth()
self.initLayers()
self.initDataLayers()
self.initTextLayers()
}
override func layoutSubviews()
{
super.layoutSubviews()
self.commonInit()
}
fileprivate func initTextLayers()
{
for textLayer in self.textLayers
{
textLayer.removeFromSuperlayer()
}
let fontSize: CGFloat = self.getFontSize()
for i in 0 ... self.segments
{
let orientation = CGFloat(i) * (1.0 / CGFloat(self.segments))
let span = self.max + abs(self.min)
let step = span / CGFloat(self.segments)
let value = CGFloat(i) * step
let font = UIFont.systemFont(ofSize: fontSize, weight: .bold)
let width = Utilities.measure(Int(value).description, .zero, font)
let point = self.getLabelPosition(orientation, width)
let layer = CATextLayer()
layer.contentsScale = UIScreen.main.scale
layer.font = font
layer.foregroundColor = UIColor.black.cgColor
layer.fontSize = fontSize
layer.string = Int(value).description
layer.alignmentMode = .center
layer.frame = CGRect(origin: point, size: .init(width: 48.0, height: self.textHeight))
self.textLayers.append(layer)
self.layer.addSublayer(layer)
}
}
fileprivate func gaugeFont() -> UIFont
{
let valueFontSize = self.getFontSize()
return UIFont.boldSystemFont(ofSize: valueFontSize)
}
fileprivate func getFontSize() -> CGFloat
{
if self.bounds.height < 128.0
{
return 10.0
}
else if self.bounds.height < 256.0
{
return 14.0
}
else
{
return 18.0
}
}
fileprivate func initDataLayers()
{
self.maskingLayer.removeFromSuperlayer()
let fillPath = self.createPath()
self.maskingLayer.frame = self.bounds
self.maskingLayer.path = fillPath.cgPath
self.maskingLayer.lineCap = .round
self.maskingLayer.fillColor = UIColor.clear.cgColor
self.maskingLayer.strokeColor = UIColor.black.cgColor
self.maskingLayer.lineWidth = self.lineWidth / 2.0
self.maskingLayer.position = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
self.layer.addSublayer(self.maskingLayer)
}
fileprivate func calculateAngle(_ value: CGFloat) -> CGFloat
{
let diff = abs(self.min) + self.max
return value / diff
}
fileprivate func getLabelPosition(_ progress: CGFloat, _ width: CGFloat) -> CGPoint
{
let size = Swift.min(self.bounds.width - self.lineWidth, self.bounds.height - self.lineWidth)
let center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
let alpha = (180.0 - self.segmentSize) / 2.0
let radius = size / 2.0 - self.lineWidth - width
let cx = center.x
let cy = center.y
let angle = self.segmentSize * progress
let x2 = self.deg2rad(180.0 + alpha + angle)
let outerX = cx + (radius + self.lineWidth / 2.0) * CGFloat(cos(x2))
let outerY = cy + (radius + self.lineWidth / 2.0) * CGFloat(sin(x2))
return CGPoint(x: outerX, y: outerY)
}
fileprivate func initLayers()
{
self.shapeLayer.removeFromSuperlayer()
let path = self.createPath()
self.shapeLayer = CAShapeLayer()
self.shapeLayer.frame = self.bounds
self.shapeLayer.path = path.cgPath
self.shapeLayer.strokeColor = UIColor.lightGray.cgColor
self.shapeLayer.fillColor = nil
self.shapeLayer.lineWidth = self.lineWidth / 2.0
self.shapeLayer.lineCap = .round
self.layer.addSublayer(self.shapeLayer)
}
fileprivate func createPath() -> UIBezierPath
{
let size = Swift.min(self.frame.width - self.lineWidth / 2, self.frame.height - self.lineWidth)
let center = CGPoint(x: self.frame.width / 2.0, y: self.frame.height / 2.0)
let alpha = (180.0 - self.segmentSize) / 2.0
let path = UIBezierPath(arcCenter: center, radius: size / 2.0, startAngle: self.deg2rad(180.0 + alpha), endAngle: self.deg2rad(360.0 - alpha), clockwise: true)
return path
}
fileprivate func determineLineWidth()
{
if self.bounds.height < 192.0
{
self.lineWidth = 20.0
}
else if self.bounds.height < 320
{
self.lineWidth = 32.0
}
else
{
self.lineWidth = 40.0
}
}
fileprivate func deg2rad(_ number: CGFloat) -> CGFloat
{
return number * .pi / 180
}
}
But I want the text to be positioned perfectly like this:
I tried adding various offsets manually, but when the control gets resized, it started to look bad again. Is there some kind of formula which I can use to calculate the exact position?
Upvotes: 0
Views: 188
Reputation: 13266
It looks like getLabelPosition
returns a point that should be used as the centre of the text but you're passing it to the frame so it's used as the upper left point.
You need to offset the point by hals the size of the label to get the origin.
let size = CGSize(width: 48.0, height: self.textHeight)
var origin = point
origin.x -= size.width / 2
origin.y -= size.height / 2
layer.frame = CGRect(origin: origin, size: size)
Upvotes: 2