how to Curve label text in iOS swift - ios

I have a label which I want to make curve up and curve down using slider. I achieve this using following code:
func drawCurvedString(on layer: CALayer, text: NSAttributedString, angle: CGFloat, radius: CGFloat) {
var radAngle = angle.radians
let textSize = text.boundingRect(
with: CGSize(width: .max, height: .max),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil)
.integral
.size
let perimeter: CGFloat = 2 * .pi * radius
let textAngle: CGFloat = textSize.width / perimeter * 2 * .pi
var textRotation: CGFloat = 0
var textDirection: CGFloat = 0
if angle > CGFloat(10).radians, angle < CGFloat(170).radians {
// bottom string
textRotation = 0.5 * .pi
textDirection = -2 * .pi
radAngle += textAngle / 2
} else {
// top string
textRotation = 1.5 * .pi
textDirection = 2 * .pi
radAngle -= textAngle / 2
}
for c in 0..<text.length {
let letter = text.attributedSubstring(from: NSRange(c..<c+1))
let charSize = letter.boundingRect(
with: CGSize(width: .max, height: .max),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil)
.integral
.size
let letterAngle = (charSize.width / perimeter) * textDirection
let x = radius * cos(radAngle + (letterAngle / 2))
let y = radius * sin(radAngle + (letterAngle / 2))
let singleChar = drawText(
on: layer,
text: letter,
frame: CGRect(
x: (layer.frame.size.width / 2) - (charSize.width / 2) + x,
y: (layer.frame.size.height / 2) - (charSize.height / 2) + y,
width: charSize.width,
height: charSize.height))
layer.addSublayer(singleChar)
singleChar.transform = CATransform3DMakeAffineTransform(CGAffineTransform(rotationAngle: radAngle - textRotation))
radAngle += letterAngle
}
}
func drawText(on layer: CALayer, text: NSAttributedString, frame: CGRect) -> CATextLayer {
let textLayer = CATextLayer()
textLayer.frame = frame
textLayer.string = text
textLayer.alignmentMode = kCAAlignmentCenter
textLayer.contentsScale = UIScreen.main.scale
return textLayer
}
But the problem is its add a sublayer of text. All I want to make existing text curve using slider value. There is some code available but that is in objective-c so can anyone help how to achieve this without adding sublayer in swift.
Thanks in advance.

Here is a complete example...
View Controller
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let testLabel = UILabelX()
testLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain original image view Top / Leading / Trailing
testLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
testLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
testLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// let's use the image's aspect ratio
testLabel.heightAnchor.constraint(equalToConstant: 300.0),
])
testLabel.backgroundColor = .yellow
testLabel.text = "This is a test of the UILabelX subclass."
}
}
UILabel subclass - from: https://stackoverflow.com/a/69056167/6257435
#IBDesignable
class UILabelX: UILabel {
#IBInspectable var angle: CGFloat = 1.6
#IBInspectable var clockwise: Bool = true
override func draw(_ rect: CGRect) {
centreArcPerpendicular()
}
/**
This draws the self.text around an arc of radius r,
with the text centred at polar angle theta
*/
func centreArcPerpendicular() {
guard let context = UIGraphicsGetCurrentContext() else { return }
let str = self.text ?? ""
let size = self.bounds.size
context.translateBy(x: size.width / 2, y: size.height / 2)
let radius = getRadiusForLabel()
let l = str.count
// let attributes: [String : Any] = [NSAttributedString.Key: self.font]
let attributes : [NSAttributedString.Key : Any] = [.font : self.font]
let characters: [String] = str.map { String($0) } // An array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
// arcs = [chordToArc(characters[i].widthOfString(usingFont: self.font), radius: radius)]
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: radius)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat(Double.pi/2) : CGFloat(Double.pi/2)
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = angle - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centre with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: radius, angle: thetaI, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
/**
This draws the String str centred at the position
specified by the polar coordinates (r, theta)
i.e. the x= r * cos(theta) y= r * sin(theta)
and rotated by the angle slantAngle
*/
func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, slantAngle: CGFloat) {
// Set the text attributes
let attributes = [NSAttributedString.Key.font: self.font!] as [NSAttributedString.Key : Any]
// Save the context
context.saveGState()
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(withAttributes: attributes)
// Move the origin by half the size of the text
context.translateBy(x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
func getRadiusForLabel() -> CGFloat {
// Imagine the bounds of this label will have a circle inside it.
// The circle will be as big as the smallest width or height of this label.
// But we need to fit the size of the font on the circle so make the circle a little
// smaller so the text does not get drawn outside the bounds of the circle.
let smallestWidthOrHeight = min(self.bounds.size.height, self.bounds.size.width)
let heightOfFont = self.text?.size(withAttributes: [NSAttributedString.Key.font: self.font]).height ?? 0
// Dividing the smallestWidthOrHeight by 2 gives us the radius for the circle.
return (smallestWidthOrHeight/2) - heightOfFont + 5
}
}

Related

TextAttribute not getting applied

I have 3 function where Im drawing text on Image. The text includes attribute paragraphStyle = .right, but the text always getting aligned to left.
These are my function :
Function Name drawText() :
private func drawText(file: String = #file , line: Int = #line, function: String = #function) {
var myInflectionRate = inflectionRate
let EPS = 0.00000001
if myInflectionRate >= 0 {
myInflectionRate = max(EPS,myInflectionRate)
}else if myInflectionRate < 0{
myInflectionRate = min(-EPS,myInflectionRate)
}
let attributedText = NSAttributedString(string: text, attributes: textAttributes)
var rect = attributedText.boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil)
let nsText = text as NSString
let textSize = nsText.size(withAttributes: textAttributes)
let angle = myInflectionRate*2*Double.pi
let arcLength = textSize.width
let r = arcLength / angle
let min_r = arcLength/(2*Double.pi)
let angleB = (Double.pi - angle) / 2.0
let widthOfRect = max(abs(angle) < Double.pi ? r * (sin(angle)/sin(angleB)) : abs(2*r), 2.0*min_r)
let positiveR = abs(r)
let positiveAngle = abs(angle)
let heightOfRect = max( positiveAngle < Double.pi ? positiveR - sqrt(positiveR*positiveR - (widthOfRect/2)*(widthOfRect/2)) : positiveR + positiveR*sin((positiveAngle-Double.pi)/2.0), 2.0*0)
rect.size = CGSize(width: widthOfRect + textSize.height, height: heightOfRect + 2.0*textSize.height)
if text.isEmpty {
rect.size = CGSize(width: 80, height: 80)
}
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
guard let context = UIGraphicsGetCurrentContext() else {
return
}
if myInflectionRate >= EPS {
context.translateBy (x: rect.size.width / 2, y: textSize.height + r )
}else if myInflectionRate <= -EPS{
context.translateBy (x: rect.size.width / 2, y: rect.size.height - textSize.height + r )
}else{
context.translateBy (x: rect.size.width / 2, y: rect.size.height/2)
}
context.scaleBy(x: 1, y: -1)
context.setFillColor(UIColor.clear.cgColor)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
imageViewForText?.image = image
imageViewForText?.contentMode = .scaleAspectFit
imageViewForText?.frame = self.bounds.insetBy(dx: globalInset!, dy: globalInset!)
imageViewForText?.backgroundColor = textBackgroundColor?.withAlphaComponent(textBackgroundAlpha ?? 1)
refresh()
}
Function Name centreArcPerpendicular() :
func centreArcPerpendicular(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, attributes: [NSAttributedString.Key: AnyObject], clockwise: Bool, size: CGSize){
let characters: [String] = str.map { String($0) } // An array of single character strings, each character in str
let l = characters.count
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
for i in 0 ..< l {
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: r)]
totalArc += arcs[i]
}
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection: CGFloat = clockwise ? -.pi / 2 : .pi / 2
var thetaI = theta - direction * totalArc / 2
var modifiedAttributes = attributes
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
let ColorArraySize = CurrentTextConfiguration.shared.textConfiguration.textureTextColor.count
let offset = characters[i].size(withAttributes: attributes)
modifiedAttributes[NSAttributedString.Key.foregroundColor] = CurrentTextConfiguration.shared.textConfiguration.textureTextColor[i%ColorArraySize].getColor(bounds: CGRect(origin: .zero, size: offset))
thetaI += direction * arcs[i] / 2
}
centre(text: str, context: context, radius: r, angle: theta, attributes: modifiedAttributes, slantAngle: thetaI + slantCorrection,size : size)
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
if (chord/(2*radius)) > 1 {
return 2 * asin(1)
}else if (chord/(2*radius)) < -1 {
return 2 * asin(-1)
}
return 2 * asin(chord / (2 * radius))
}
Function name center() :
func centre(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, attributes: [NSAttributedString.Key: AnyObject], slantAngle: CGFloat, size : CGSize) {
NSAttributedString.Key.font: font]
context.saveGState()
context.scaleBy(x: 1, y: -1)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
context.rotate(by: -slantAngle)
let offset = str.size(withAttributes: attributes)
context.translateBy (x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
context.restoreGState()
}
I want to align my text from right , not left

How to curve to label text? [duplicate]

I am looking for some up to date help/hints on how to draw simple single line strings around the edge of a circle using Swift2 for iOS9. I see quite dated examples involving old ObjC fragments, and oft limited to OS X only. Is this even possible in iOS within a custom UIView subclass's drawRect() method?
I was going to say "What have you tried?", but it's Friday afternoon and I got off work early, so I took the opportunity to translate my old ObjC code. Here it is, suitable for Playground. It should be trivial to put it in your UIView.
Swift 2
See below for Swift 3 & Swift 4 updates...
import UIKit
func centreArcPerpendicularText(str: String, context: CGContextRef, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool){
// *******************************************************
// This draws the String str around an arc of radius r,
// with the text centred at polar angle theta
// *******************************************************
let l = str.characters.count
let attributes = [NSFontAttributeName: font]
var characters: [String] = [] // This will be an array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
characters += [String(str[str.startIndex.advancedBy(i)])]
arcs += [chordToArc(characters[i].sizeWithAttributes(attributes).width, radius: r)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat(M_PI_2) : CGFloat(M_PI_2)
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = theta - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centerText with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centreText(characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
func centreText(str: String, context: CGContextRef, radius r:CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat) {
// *******************************************************
// This draws the String str centred at the position
// specified by the polar coordinates (r, theta)
// i.e. the x= r * cos(theta) y= r * sin(theta)
// and rotated by the angle slantAngle
// *******************************************************
// Set the text attributes
let attributes = [NSForegroundColorAttributeName: c,
NSFontAttributeName: font]
// Save the context
CGContextSaveGState(context)
// Undo the inversion of the Y-axis (or the text goes backwards!)
CGContextScaleCTM(context, 1, -1)
// Move the origin to the centre of the text (negating the y-axis manually)
CGContextTranslateCTM(context, r * cos(theta), -(r * sin(theta)))
// Rotate the coordinate system
CGContextRotateCTM(context, -slantAngle)
// Calculate the width of the text
let offset = str.sizeWithAttributes(attributes)
// Move the origin by half the size of the text
CGContextTranslateCTM (context, -offset.width / 2, -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.drawAtPoint(CGPointZero, withAttributes: attributes)
// Restore the context
CGContextRestoreGState(context)
}
// *******************************************************
// Playground code to test
// *******************************************************
let size = CGSize(width: 256, height: 256)
UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
let context = UIGraphicsGetCurrentContext()!
// *******************************************************************
// Scale & translate the context to have 0,0
// at the centre of the screen maths convention
// Obviously change your origin to suit...
// *******************************************************************
CGContextTranslateCTM (context, size.width / 2, size.height / 2)
CGContextScaleCTM (context, 1, -1)
centreArcPerpendicularText("Hello round world", context: context, radius: 100, angle: 0, colour: UIColor.redColor(), font: UIFont.systemFontOfSize(16), clockwise: true)
centreArcPerpendicularText("Anticlockwise", context: context, radius: 100, angle: CGFloat(-M_PI_2), colour: UIColor.redColor(), font: UIFont.systemFontOfSize(16), clockwise: false)
centreText("Hello flat world", context: context, radius: 0, angle: 0 , colour: UIColor.yellowColor(), font: UIFont.systemFontOfSize(16), slantAngle: CGFloat(M_PI_4))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
Output is:
Update
Added clockwise / anticlockwise & straight example.
Update Swift 3
func centreArcPerpendicular(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool){
// *******************************************************
// This draws the String str around an arc of radius r,
// with the text centred at polar angle theta
// *******************************************************
let l = str.characters.count
let attributes = [NSFontAttributeName: font]
let characters: [String] = str.characters.map { String($0) } // An array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
arcs += [chordToArc(characters[i].size(attributes: attributes).width, radius: r)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat(M_PI_2) : CGFloat(M_PI_2)
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = theta - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centerText with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat) {
// *******************************************************
// This draws the String str centred at the position
// specified by the polar coordinates (r, theta)
// i.e. the x= r * cos(theta) y= r * sin(theta)
// and rotated by the angle slantAngle
// *******************************************************
// Set the text attributes
let attributes = [NSForegroundColorAttributeName: c,
NSFontAttributeName: font]
// Save the context
context.saveGState()
// Undo the inversion of the Y-axis (or the text goes backwards!)
context.scaleBy(x: 1, y: -1)
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(attributes: attributes)
// Move the origin by half the size of the text
context.translateBy (x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
// *******************************************************
// Playground code to test
// *******************************************************
let size = CGSize(width: 256, height: 256)
UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
let context = UIGraphicsGetCurrentContext()!
// *******************************************************************
// Scale & translate the context to have 0,0
// at the centre of the screen maths convention
// Obviously change your origin to suit...
// *******************************************************************
context.translateBy (x: size.width / 2, y: size.height / 2)
context.scaleBy (x: 1, y: -1)
centreArcPerpendicular(text: "Hello round world", context: context, radius: 100, angle: 0, colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: true)
centreArcPerpendicular(text: "Anticlockwise", context: context, radius: 100, angle: CGFloat(-M_PI_2), colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: false)
centre(text: "Hello flat world", context: context, radius: 0, angle: 0 , colour: UIColor.yellow, font: UIFont.systemFont(ofSize: 16), slantAngle: CGFloat(M_PI_4))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
Swift 4
Yet again, minor changes, this time fixing the deprecation of M_PI, String's abandonment of .characters, the parameter label change in .size(withAttributes..., and the change in text attributes to the NSAttributedStringKey enum...
import UIKit
func centreArcPerpendicular(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool){
// *******************************************************
// This draws the String str around an arc of radius r,
// with the text centred at polar angle theta
// *******************************************************
let characters: [String] = str.map { String($0) } // An array of single character strings, each character in str
let l = characters.count
let attributes = [NSAttributedStringKey.font: font]
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: r)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection: CGFloat = clockwise ? -.pi / 2 : .pi / 2
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = theta - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centerText with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
func centre(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat) {
// *******************************************************
// This draws the String str centred at the position
// specified by the polar coordinates (r, theta)
// i.e. the x= r * cos(theta) y= r * sin(theta)
// and rotated by the angle slantAngle
// *******************************************************
// Set the text attributes
let attributes = [NSAttributedStringKey.foregroundColor: c, NSAttributedStringKey.font: font]
//let attributes = [NSForegroundColorAttributeName: c, NSFontAttributeName: font]
// Save the context
context.saveGState()
// Undo the inversion of the Y-axis (or the text goes backwards!)
context.scaleBy(x: 1, y: -1)
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(withAttributes: attributes)
// Move the origin by half the size of the text
context.translateBy (x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
// *******************************************************
// Playground code to test
// *******************************************************
let size = CGSize(width: 256, height: 256)
UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
let context = UIGraphicsGetCurrentContext()!
// *******************************************************************
// Scale & translate the context to have 0,0
// at the centre of the screen maths convention
// Obviously change your origin to suit...
// *******************************************************************
context.translateBy (x: size.width / 2, y: size.height / 2)
context.scaleBy(x: 1, y: -1)
centreArcPerpendicular(text: "Hello round 🌏 world", context: context, radius: 100, angle: 0, colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: true)
centreArcPerpendicular(text: "Anticlockwise", context: context, radius: 100, angle: CGFloat(-M_PI_2), colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: false)
centre(text: "Hello flat world", context: context, radius: 0, angle: 0 , colour: UIColor.yellow, font: UIFont.systemFont(ofSize: 16), slantAngle: .pi / 4)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
Update to show use in UIView
Commentator #RitvikUpadhyaya asks how to do this in a UIView - obvious to old hands, but not perhaps to beginners. The trick is to get the right context using UIGraphicsGetCurrentContext without calling UIGraphicsBeginImageContextWithOptions (which overrides the UIView's context as the current context) - therefore your UIView should look like this:
class MyView: UIView {
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let size = self.bounds.size
context.translateBy (x: size.width / 2, y: size.height / 2)
context.scaleBy (x: 1, y: -1)
centreArcPerpendicular(text: "Hello round world", context: context, radius: 100, angle: 0, colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: true)
centreArcPerpendicular(text: "Anticlockwise", context: context, radius: 100, angle: CGFloat(-M_PI_2), colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: false)
centre(text: "Hello flat world", context: context, radius: 0, angle: 0 , colour: UIColor.yellow, font: UIFont.systemFont(ofSize: 16), slantAngle: CGFloat(M_PI_4))
}
}
#IBDesignable For UILabel on Circular Path
First of all, I think we can all agree that #Grimxn is THE MAN! His solution kicks butt. I took his work and refactored it into a custom UILabel control that you can set and edit on the Storyboard. If you guys watch my videos you know how much I love to do this stuff! 😀
Swift 3 Code for Custom UILabel
import UIKit
#IBDesignable
class UILabelX: UILabel {
// *******************************************************
// DEFINITIONS (Because I'm not brilliant and I'll forget most this tomorrow.)
// Radius: A straight line from the center to the circumference of a circle.
// Circumference: The distance around the edge (outer line) the circle.
// Arc: A part of the circumference of a circle. Like a length or section of the circumference.
// Theta: A label or name that represents an angle.
// Subtend: A letter has a width. If you put the letter on the circumference, the letter's width
// gives you an arc. So now that you have an arc (a length on the circumference) you can
// use that to get an angle. You get an angle when you draw a line from the center of the
// circle to each end point of your arc. So "subtend" means to get an angle from an arc.
// Chord: A line segment connecting two points on a curve. If you have an arc then there is a
// start point and an end point. If you draw a straight line from start point to end point
// then you have a "chord".
// sin: (Super simple/incomplete definition) Or "sine" takes an angle in degrees and gives you a number.
// asin: Or "asine" takes a number and gives you an angle in degrees. Opposite of sine.
// More complete definition: http://www.mathsisfun.com/sine-cosine-tangent.html
// cosine: Also takes an angle in degrees and gives you another number from using the two radiuses (radii).
// *******************************************************
#IBInspectable var angle: CGFloat = 1.6
#IBInspectable var clockwise: Bool = true
override func draw(_ rect: CGRect) {
centreArcPerpendicular()
}
/**
This draws the self.text around an arc of radius r,
with the text centred at polar angle theta
*/
func centreArcPerpendicular() {
guard let context = UIGraphicsGetCurrentContext() else { return }
let str = self.text ?? ""
let size = self.bounds.size
context.translateBy(x: size.width / 2, y: size.height / 2)
let radius = getRadiusForLabel()
let l = str.characters.count
let attributes: [String : Any] = [NSFontAttributeName: self.font]
let characters: [String] = str.characters.map { String($0) } // An array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
arcs += [chordToArc(characters[i].size(attributes: attributes).width, radius: radius)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat(M_PI_2) : CGFloat(M_PI_2)
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = angle - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centre with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: radius, angle: thetaI, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
/**
This draws the String str centred at the position
specified by the polar coordinates (r, theta)
i.e. the x= r * cos(theta) y= r * sin(theta)
and rotated by the angle slantAngle
*/
func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, slantAngle: CGFloat) {
// Set the text attributes
let attributes = [NSForegroundColorAttributeName: self.textColor,
NSFontAttributeName: self.font] as [String : Any]
// Save the context
context.saveGState()
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(attributes: attributes)
// Move the origin by half the size of the text
context.translateBy(x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
func getRadiusForLabel() -> CGFloat {
// Imagine the bounds of this label will have a circle inside it.
// The circle will be as big as the smallest width or height of this label.
// But we need to fit the size of the font on the circle so make the circle a little
// smaller so the text does not get drawn outside the bounds of the circle.
let smallestWidthOrHeight = min(self.bounds.size.height, self.bounds.size.width)
let heightOfFont = self.text?.size(attributes: [NSFontAttributeName: self.font]).height ?? 0
// Dividing the smallestWidthOrHeight by 2 gives us the radius for the circle.
return (smallestWidthOrHeight/2) - heightOfFont + 5
}
}
Example of Usage on Storyboard
Changes I made
I removed parameters that I could get straight from the label now.
I'm admittedly not the smartest in Trigonometry and have forgotten a lot at my age so I included all the relevant definitions so I could start to understand #Grimxn brilliancy.
The angle and clockwise settings are now properties you can adjust in Attributes Inspector.
I create the radius from the size of the label now.
Put some of the comments in standard format on functions, you know, so you get that popup that comes up with you OPTION + CLICK functions.
Problems I have seen
I encourage you to edit the above to improve it.
I don't know why but sometimes the label kept rendering over other controls even though it was behind them in the document outline.
Always the same implementation but adjusted for Swift 4
import UIKit
#IBDesignable
class CircularLabel: UILabel {
// *******************************************************
// DEFINITIONS (Because I'm not brilliant and I'll forget most this tomorrow.)
// Radius: A straight line from the center to the circumference of a circle.
// Circumference: The distance around the edge (outer line) the circle.
// Arc: A part of the circumference of a circle. Like a length or section of the circumference.
// Theta: A label or name that represents an angle.
// Subtend: A letter has a width. If you put the letter on the circumference, the letter's width
// gives you an arc. So now that you have an arc (a length on the circumference) you can
// use that to get an angle. You get an angle when you draw a line from the center of the
// circle to each end point of your arc. So "subtend" means to get an angle from an arc.
// Chord: A line segment connecting two points on a curve. If you have an arc then there is a
// start point and an end point. If you draw a straight line from start point to end point
// then you have a "chord".
// sin: (Super simple/incomplete definition) Or "sine" takes an angle in degrees and gives you a number.
// asin: Or "asine" takes a number and gives you an angle in degrees. Opposite of sine.
// More complete definition: http://www.mathsisfun.com/sine-cosine-tangent.html
// cosine: Also takes an angle in degrees and gives you another number from using the two radiuses (radii).
// *******************************************************
#IBInspectable var angle: CGFloat = 1.6
#IBInspectable var clockwise: Bool = true
override func draw(_ rect: CGRect) {
centreArcPerpendicular()
}
/**
This draws the self.text around an arc of radius r,
with the text centred at polar angle theta
*/
func centreArcPerpendicular() {
guard let context = UIGraphicsGetCurrentContext() else { return }
let string = text ?? ""
let size = bounds.size
context.translateBy(x: size.width / 2, y: size.height / 2)
let radius = getRadiusForLabel()
let l = string.count
let attributes = [NSAttributedStringKey.font : self.font!]
let characters: [String] = string.map { String($0) } // An array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: radius)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat.pi/2 : CGFloat.pi/2
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = angle - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centre with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: radius, angle: thetaI, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
/**
This draws the String str centred at the position
specified by the polar coordinates (r, theta)
i.e. the x= r * cos(theta) y= r * sin(theta)
and rotated by the angle slantAngle
*/
func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, slantAngle: CGFloat) {
// Set the text attributes
let attributes : [NSAttributedStringKey : Any] = [
NSAttributedStringKey.foregroundColor: textColor!,
NSAttributedStringKey.font: font!
]
// Save the context
context.saveGState()
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(withAttributes: attributes)
// Move the origin by half the size of the text
context.translateBy(x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
func getRadiusForLabel() -> CGFloat {
// Imagine the bounds of this label will have a circle inside it.
// The circle will be as big as the smallest width or height of this label.
// But we need to fit the size of the font on the circle so make the circle a little
// smaller so the text does not get drawn outside the bounds of the circle.
let smallestWidthOrHeight = min(bounds.size.height, bounds.size.width)
let heightOfFont = text?.size(withAttributes: [NSAttributedStringKey.font: self.font]).height ?? 0
// Dividing the smallestWidthOrHeight by 2 gives us the radius for the circle.
return (smallestWidthOrHeight/2) - heightOfFont + 5
}
}
#IBDesignable For UILabel on Circular Path for Swift 2
Big thanks to both #Grimxn and #mark-moeykens for the absolutely killer work. I've done a small refactor on Mark's work so I could use it in a project that hasn't taken the time to update to Swift 3. Wanted to share, since the previous posts were so helpful.
Swift 2 Code for Custom UILabel
import UIKit
#IBDesignable
class ArcUILabel: UILabel
{
// *******************************************************
// DEFINITIONS (Because I'm not brilliant and I'll forget most this tomorrow.)
// Radius: A straight line from the center to the circumference of a circle.
// Circumference: The distance around the edge (outer line) the circle.
// Arc: A part of the circumference of a circle. Like a length or section of the circumference.
// Theta: A label or name that represents an angle.
// Subtend: A letter has a width. If you put the letter on the circumference, the letter's width
// gives you an arc. So now that you have an arc (a length on the circumference) you can
// use that to get an angle. You get an angle when you draw a line from the center of the
// circle to each end point of your arc. So "subtend" means to get an angle from an arc.
// Chord: A line segment connecting two points on a curve. If you have an arc then there is a
// start point and an end point. If you draw a straight line from start point to end point
// then you have a "chord".
// sin: (Super simple/incomplete definition) Or "sine" takes an angle in degrees and gives you a number.
// asin: Or "asine" takes a number and gives you an angle in degrees. Opposite of sine.
// More complete definition: http://www.mathsisfun.com/sine-cosine-tangent.html
// cosine: Also takes an angle in degrees and gives you another number from using the two radiuses (radii).
// *******************************************************
#IBInspectable var angle: CGFloat = 1.6
#IBInspectable var clockwise: Bool = true
override func drawRect(rect: CGRect)
{
centreArcPerpendicular()
}
/**
This draws the self.text around an arc of radius r,
with the text centred at polar angle theta
*/
func centreArcPerpendicular() {
guard let context = UIGraphicsGetCurrentContext() else { return }
let str = self.text ?? ""
let size = self.bounds.size
CGContextTranslateCTM(context, size.width / 2, size.height / 2)
let radius = getRadiusForLabel()
let l = str.characters.count
let attributes: [String : AnyObject] = [NSFontAttributeName: self.font]
let characters: [String] = str.characters.map { String($0) } // An array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
arcs += [chordToArc(characters[i].sizeWithAttributes(attributes).width, radius: radius)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat(M_PI_2) : CGFloat(M_PI_2)
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = angle - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centre with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: radius, angle: thetaI, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
/**
This draws the String str centred at the position
specified by the polar coordinates (r, theta)
i.e. the x= r * cos(theta) y= r * sin(theta)
and rotated by the angle slantAngle
*/
func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, slantAngle: CGFloat) {
// Set the text attributes
let attributes = [NSForegroundColorAttributeName: self.textColor,
NSFontAttributeName: self.font] as [String : AnyObject]
// Save the context
CGContextSaveGState(context)
// Move the origin to the centre of the text (negating the y-axis manually)
CGContextTranslateCTM(context, r * cos(theta), -(r * sin(theta)))
// Rotate the coordinate system
CGContextRotateCTM(context, -slantAngle)
// Calculate the width of the text
let offset: CGSize = str.sizeWithAttributes(attributes)
// Move the origin by half the size of the text
CGContextTranslateCTM(context, -offset.width / 2, -offset.height / 2)
// Draw the text
let txtStr = NSString(string: str)
txtStr.drawAtPoint(CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
CGContextRestoreGState(context)
}
func getRadiusForLabel() -> CGFloat {
// Imagine the bounds of this label will have a circle inside it.
// The circle will be as big as the smallest width or height of this label.
// But we need to fit the size of the font on the circle so make the circle a little
// smaller so the text does not get drawn outside the bounds of the circle.
let smallestWidthOrHeight = min(self.bounds.size.height, self.bounds.size.width)
let heightOfFont = self.text?.sizeWithAttributes([NSFontAttributeName: self.font]).height ?? 0
// Dividing the smallestWidthOrHeight by 2 gives us the radius for the circle.
return (smallestWidthOrHeight/2) - heightOfFont + 5
}
}
A C#-version based on the code from Grimxm:
private void CenterArcPerpendicular(string text, CGContext context, float radius, double angle, UIColor textColor, UIFont font, bool isClockwise) {
var characters = text.ToCharArray();
var arcs = new List<float>() { };
float totalArc = 0;
for (var i = 0; i < characters.Length; i++)
{
var character = new NSString(new string(new char[] { characters[i] }));
var charSize = character.StringSize(font);
var arc = ChordToArc((float)charSize.Width, radius);
arcs.Add(arc);
totalArc += arc;
}
var direction = isClockwise ? -1 : 1;
var slantCorrection = (float)(isClockwise ? -(Math.PI / 2) : (Math.PI / 2));
var thetaI = angle - (direction * (totalArc / 2));
for (var i = 0; i < characters.Length; i++)
{
var character = new NSString(new string(new char[] { characters[i] }));
thetaI += direction * arcs[i] / 2;
CenterText(character, context: context, radius: radius, angle: thetaI, textColor: textColor, font: font, slantAngle: thetaI + slantCorrection);
thetaI += direction * arcs[i] / 2;
}
}
private float ChordToArc(float chord, float radius) {
return (float)(2 * Math.Asin(chord / (2 * radius)));
}
private void CenterText(NSString text, CGContext context, float radius, double angle, UIColor textColor, UIFont font, double slantAngle)
{
var attributes = new UIStringAttributes { Font = font, ForegroundColor = textColor };
context.SaveState();
context.ScaleCTM(1, -1);
var dX = radius * Math.Cos(angle);
var dY = -(radius * Math.Sin(angle));
context.TranslateCTM((nfloat)dX, (nfloat)dY);
context.RotateCTM(-(nfloat)slantAngle);
var offset = text.StringSize(font);
context.TranslateCTM(-offset.Width / 2, -offset.Height / 2);
text.DrawString(CGPoint.Empty, attributes);
context.RestoreState();
}
For use in a Xamarin iOS app.
Update code to Swift 5
import Foundation
import UIKit
#IBDesignable
class UILabelX: UILabel {
#IBInspectable var angle: CGFloat = 1.6
#IBInspectable var clockwise: Bool = true
override func draw(_ rect: CGRect) {
centreArcPerpendicular()
}
/**
This draws the self.text around an arc of radius r,
with the text centred at polar angle theta
*/
func centreArcPerpendicular() {
guard let context = UIGraphicsGetCurrentContext() else { return }
let str = self.text ?? ""
let size = self.bounds.size
context.translateBy(x: size.width / 2, y: size.height / 2)
let radius = getRadiusForLabel()
let l = str.count
// let attributes: [String : Any] = [NSAttributedString.Key: self.font]
let attributes : [NSAttributedString.Key : Any] = [.font : self.font]
let characters: [String] = str.map { String($0) } // An array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
// arcs = [chordToArc(characters[i].widthOfString(usingFont: self.font), radius: radius)]
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: radius)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat(Double.pi/2) : CGFloat(Double.pi/2)
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = angle - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centre with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: radius, angle: thetaI, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
/**
This draws the String str centred at the position
specified by the polar coordinates (r, theta)
i.e. the x= r * cos(theta) y= r * sin(theta)
and rotated by the angle slantAngle
*/
func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, slantAngle: CGFloat) {
// Set the text attributes
let attributes = [NSAttributedString.Key.font: self.font!] as [NSAttributedString.Key : Any]
// Save the context
context.saveGState()
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(withAttributes: attributes)
// Move the origin by half the size of the text
context.translateBy(x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
func getRadiusForLabel() -> CGFloat {
// Imagine the bounds of this label will have a circle inside it.
// The circle will be as big as the smallest width or height of this label.
// But we need to fit the size of the font on the circle so make the circle a little
// smaller so the text does not get drawn outside the bounds of the circle.
let smallestWidthOrHeight = min(self.bounds.size.height, self.bounds.size.width)
let heightOfFont = self.text?.size(withAttributes: [NSAttributedString.Key.font: self.font]).height ?? 0
// Dividing the smallestWidthOrHeight by 2 gives us the radius for the circle.
return (smallestWidthOrHeight/2) - heightOfFont + 5
}
}
Swift 5
Transparent background
Kern option for text
Correct ratio for view
Full playground code
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
// *******************************************************
// Playground code to test
// *******************************************************
let size = CGSize(width: 256, height: 256)
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
let context = UIGraphicsGetCurrentContext()!
// *******************************************************************
// Scale & translate the context to have 0,0
// at the centre of the screen maths convention
// Obviously change your origin to suit...
// *******************************************************************
context.translateBy (x: size.width / 2, y: size.height / 2)
context.scaleBy(x: 1, y: -1)
centreArcPerpendicular(text: "Hello round World",
context: context,
radius: size.height * 0.44,
angle: 0,
colour: UIColor.white,
font: UIFont.systemFont(ofSize: 16),
clockwise: true,
kern: 3)
centreArcPerpendicular(text: "Anticlockwise",
context: context,
radius: size.height * 0.44,
angle: -.pi,
colour: UIColor.white,
font: UIFont.systemFont(ofSize: 16),
clockwise: false,
kern: 3)
centre(text: "Hello center world", context: context, radius: 0, angle: 0 , colour: UIColor.yellow, font: UIFont.systemFont(ofSize: 16), slantAngle: .pi/4, kern: 0)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
func centreArcPerpendicular(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool, kern: CGFloat = 0) {
// *******************************************************
// This draws the String str around an arc of radius r,
// with the text centred at polar angle theta
// *******************************************************
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
return 2 * asin(chord / (2 * radius))
}
let characters: [String] = str.map { String($0) } // An array of single character strings, each character in str
let l = characters.count
let attributes = [NSAttributedString.Key.font: font,
NSAttributedString.Key.kern: kern] as [NSAttributedString.Key : Any]
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: r)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection: CGFloat = clockwise ? -.pi / 2 : .pi / 2
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = theta - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centerText with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection, kern: kern)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func centre(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat, kern: CGFloat) {
// *******************************************************
// This draws the String str centred at the position
// specified by the polar coordinates (r, theta)
// i.e. the x= r * cos(theta) y= r * sin(theta)
// and rotated by the angle slantAngle
// *******************************************************
// Set the text attributes
let attributes = [NSAttributedString.Key.foregroundColor: c,
NSAttributedString.Key.font: font,
NSAttributedString.Key.kern: kern] as [NSAttributedString.Key : Any]
//let attributes = [NSForegroundColorAttributeName: c, NSFontAttributeName: font]
// Save the context
context.saveGState()
// Undo the inversion of the Y-axis (or the text goes backwards!)
context.scaleBy(x: 1, y: -1)
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(withAttributes: attributes)
// Move the origin by half the size of the text
context.translateBy (x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

Draw text on Bezier path [duplicate]

I am looking for some up to date help/hints on how to draw simple single line strings around the edge of a circle using Swift2 for iOS9. I see quite dated examples involving old ObjC fragments, and oft limited to OS X only. Is this even possible in iOS within a custom UIView subclass's drawRect() method?
I was going to say "What have you tried?", but it's Friday afternoon and I got off work early, so I took the opportunity to translate my old ObjC code. Here it is, suitable for Playground. It should be trivial to put it in your UIView.
Swift 2
See below for Swift 3 & Swift 4 updates...
import UIKit
func centreArcPerpendicularText(str: String, context: CGContextRef, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool){
// *******************************************************
// This draws the String str around an arc of radius r,
// with the text centred at polar angle theta
// *******************************************************
let l = str.characters.count
let attributes = [NSFontAttributeName: font]
var characters: [String] = [] // This will be an array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
characters += [String(str[str.startIndex.advancedBy(i)])]
arcs += [chordToArc(characters[i].sizeWithAttributes(attributes).width, radius: r)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat(M_PI_2) : CGFloat(M_PI_2)
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = theta - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centerText with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centreText(characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
func centreText(str: String, context: CGContextRef, radius r:CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat) {
// *******************************************************
// This draws the String str centred at the position
// specified by the polar coordinates (r, theta)
// i.e. the x= r * cos(theta) y= r * sin(theta)
// and rotated by the angle slantAngle
// *******************************************************
// Set the text attributes
let attributes = [NSForegroundColorAttributeName: c,
NSFontAttributeName: font]
// Save the context
CGContextSaveGState(context)
// Undo the inversion of the Y-axis (or the text goes backwards!)
CGContextScaleCTM(context, 1, -1)
// Move the origin to the centre of the text (negating the y-axis manually)
CGContextTranslateCTM(context, r * cos(theta), -(r * sin(theta)))
// Rotate the coordinate system
CGContextRotateCTM(context, -slantAngle)
// Calculate the width of the text
let offset = str.sizeWithAttributes(attributes)
// Move the origin by half the size of the text
CGContextTranslateCTM (context, -offset.width / 2, -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.drawAtPoint(CGPointZero, withAttributes: attributes)
// Restore the context
CGContextRestoreGState(context)
}
// *******************************************************
// Playground code to test
// *******************************************************
let size = CGSize(width: 256, height: 256)
UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
let context = UIGraphicsGetCurrentContext()!
// *******************************************************************
// Scale & translate the context to have 0,0
// at the centre of the screen maths convention
// Obviously change your origin to suit...
// *******************************************************************
CGContextTranslateCTM (context, size.width / 2, size.height / 2)
CGContextScaleCTM (context, 1, -1)
centreArcPerpendicularText("Hello round world", context: context, radius: 100, angle: 0, colour: UIColor.redColor(), font: UIFont.systemFontOfSize(16), clockwise: true)
centreArcPerpendicularText("Anticlockwise", context: context, radius: 100, angle: CGFloat(-M_PI_2), colour: UIColor.redColor(), font: UIFont.systemFontOfSize(16), clockwise: false)
centreText("Hello flat world", context: context, radius: 0, angle: 0 , colour: UIColor.yellowColor(), font: UIFont.systemFontOfSize(16), slantAngle: CGFloat(M_PI_4))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
Output is:
Update
Added clockwise / anticlockwise & straight example.
Update Swift 3
func centreArcPerpendicular(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool){
// *******************************************************
// This draws the String str around an arc of radius r,
// with the text centred at polar angle theta
// *******************************************************
let l = str.characters.count
let attributes = [NSFontAttributeName: font]
let characters: [String] = str.characters.map { String($0) } // An array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
arcs += [chordToArc(characters[i].size(attributes: attributes).width, radius: r)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat(M_PI_2) : CGFloat(M_PI_2)
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = theta - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centerText with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat) {
// *******************************************************
// This draws the String str centred at the position
// specified by the polar coordinates (r, theta)
// i.e. the x= r * cos(theta) y= r * sin(theta)
// and rotated by the angle slantAngle
// *******************************************************
// Set the text attributes
let attributes = [NSForegroundColorAttributeName: c,
NSFontAttributeName: font]
// Save the context
context.saveGState()
// Undo the inversion of the Y-axis (or the text goes backwards!)
context.scaleBy(x: 1, y: -1)
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(attributes: attributes)
// Move the origin by half the size of the text
context.translateBy (x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
// *******************************************************
// Playground code to test
// *******************************************************
let size = CGSize(width: 256, height: 256)
UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
let context = UIGraphicsGetCurrentContext()!
// *******************************************************************
// Scale & translate the context to have 0,0
// at the centre of the screen maths convention
// Obviously change your origin to suit...
// *******************************************************************
context.translateBy (x: size.width / 2, y: size.height / 2)
context.scaleBy (x: 1, y: -1)
centreArcPerpendicular(text: "Hello round world", context: context, radius: 100, angle: 0, colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: true)
centreArcPerpendicular(text: "Anticlockwise", context: context, radius: 100, angle: CGFloat(-M_PI_2), colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: false)
centre(text: "Hello flat world", context: context, radius: 0, angle: 0 , colour: UIColor.yellow, font: UIFont.systemFont(ofSize: 16), slantAngle: CGFloat(M_PI_4))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
Swift 4
Yet again, minor changes, this time fixing the deprecation of M_PI, String's abandonment of .characters, the parameter label change in .size(withAttributes..., and the change in text attributes to the NSAttributedStringKey enum...
import UIKit
func centreArcPerpendicular(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool){
// *******************************************************
// This draws the String str around an arc of radius r,
// with the text centred at polar angle theta
// *******************************************************
let characters: [String] = str.map { String($0) } // An array of single character strings, each character in str
let l = characters.count
let attributes = [NSAttributedStringKey.font: font]
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: r)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection: CGFloat = clockwise ? -.pi / 2 : .pi / 2
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = theta - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centerText with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
func centre(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat) {
// *******************************************************
// This draws the String str centred at the position
// specified by the polar coordinates (r, theta)
// i.e. the x= r * cos(theta) y= r * sin(theta)
// and rotated by the angle slantAngle
// *******************************************************
// Set the text attributes
let attributes = [NSAttributedStringKey.foregroundColor: c, NSAttributedStringKey.font: font]
//let attributes = [NSForegroundColorAttributeName: c, NSFontAttributeName: font]
// Save the context
context.saveGState()
// Undo the inversion of the Y-axis (or the text goes backwards!)
context.scaleBy(x: 1, y: -1)
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(withAttributes: attributes)
// Move the origin by half the size of the text
context.translateBy (x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
// *******************************************************
// Playground code to test
// *******************************************************
let size = CGSize(width: 256, height: 256)
UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
let context = UIGraphicsGetCurrentContext()!
// *******************************************************************
// Scale & translate the context to have 0,0
// at the centre of the screen maths convention
// Obviously change your origin to suit...
// *******************************************************************
context.translateBy (x: size.width / 2, y: size.height / 2)
context.scaleBy(x: 1, y: -1)
centreArcPerpendicular(text: "Hello round 🌏 world", context: context, radius: 100, angle: 0, colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: true)
centreArcPerpendicular(text: "Anticlockwise", context: context, radius: 100, angle: CGFloat(-M_PI_2), colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: false)
centre(text: "Hello flat world", context: context, radius: 0, angle: 0 , colour: UIColor.yellow, font: UIFont.systemFont(ofSize: 16), slantAngle: .pi / 4)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
Update to show use in UIView
Commentator #RitvikUpadhyaya asks how to do this in a UIView - obvious to old hands, but not perhaps to beginners. The trick is to get the right context using UIGraphicsGetCurrentContext without calling UIGraphicsBeginImageContextWithOptions (which overrides the UIView's context as the current context) - therefore your UIView should look like this:
class MyView: UIView {
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let size = self.bounds.size
context.translateBy (x: size.width / 2, y: size.height / 2)
context.scaleBy (x: 1, y: -1)
centreArcPerpendicular(text: "Hello round world", context: context, radius: 100, angle: 0, colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: true)
centreArcPerpendicular(text: "Anticlockwise", context: context, radius: 100, angle: CGFloat(-M_PI_2), colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: false)
centre(text: "Hello flat world", context: context, radius: 0, angle: 0 , colour: UIColor.yellow, font: UIFont.systemFont(ofSize: 16), slantAngle: CGFloat(M_PI_4))
}
}
#IBDesignable For UILabel on Circular Path
First of all, I think we can all agree that #Grimxn is THE MAN! His solution kicks butt. I took his work and refactored it into a custom UILabel control that you can set and edit on the Storyboard. If you guys watch my videos you know how much I love to do this stuff! 😀
Swift 3 Code for Custom UILabel
import UIKit
#IBDesignable
class UILabelX: UILabel {
// *******************************************************
// DEFINITIONS (Because I'm not brilliant and I'll forget most this tomorrow.)
// Radius: A straight line from the center to the circumference of a circle.
// Circumference: The distance around the edge (outer line) the circle.
// Arc: A part of the circumference of a circle. Like a length or section of the circumference.
// Theta: A label or name that represents an angle.
// Subtend: A letter has a width. If you put the letter on the circumference, the letter's width
// gives you an arc. So now that you have an arc (a length on the circumference) you can
// use that to get an angle. You get an angle when you draw a line from the center of the
// circle to each end point of your arc. So "subtend" means to get an angle from an arc.
// Chord: A line segment connecting two points on a curve. If you have an arc then there is a
// start point and an end point. If you draw a straight line from start point to end point
// then you have a "chord".
// sin: (Super simple/incomplete definition) Or "sine" takes an angle in degrees and gives you a number.
// asin: Or "asine" takes a number and gives you an angle in degrees. Opposite of sine.
// More complete definition: http://www.mathsisfun.com/sine-cosine-tangent.html
// cosine: Also takes an angle in degrees and gives you another number from using the two radiuses (radii).
// *******************************************************
#IBInspectable var angle: CGFloat = 1.6
#IBInspectable var clockwise: Bool = true
override func draw(_ rect: CGRect) {
centreArcPerpendicular()
}
/**
This draws the self.text around an arc of radius r,
with the text centred at polar angle theta
*/
func centreArcPerpendicular() {
guard let context = UIGraphicsGetCurrentContext() else { return }
let str = self.text ?? ""
let size = self.bounds.size
context.translateBy(x: size.width / 2, y: size.height / 2)
let radius = getRadiusForLabel()
let l = str.characters.count
let attributes: [String : Any] = [NSFontAttributeName: self.font]
let characters: [String] = str.characters.map { String($0) } // An array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
arcs += [chordToArc(characters[i].size(attributes: attributes).width, radius: radius)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat(M_PI_2) : CGFloat(M_PI_2)
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = angle - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centre with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: radius, angle: thetaI, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
/**
This draws the String str centred at the position
specified by the polar coordinates (r, theta)
i.e. the x= r * cos(theta) y= r * sin(theta)
and rotated by the angle slantAngle
*/
func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, slantAngle: CGFloat) {
// Set the text attributes
let attributes = [NSForegroundColorAttributeName: self.textColor,
NSFontAttributeName: self.font] as [String : Any]
// Save the context
context.saveGState()
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(attributes: attributes)
// Move the origin by half the size of the text
context.translateBy(x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
func getRadiusForLabel() -> CGFloat {
// Imagine the bounds of this label will have a circle inside it.
// The circle will be as big as the smallest width or height of this label.
// But we need to fit the size of the font on the circle so make the circle a little
// smaller so the text does not get drawn outside the bounds of the circle.
let smallestWidthOrHeight = min(self.bounds.size.height, self.bounds.size.width)
let heightOfFont = self.text?.size(attributes: [NSFontAttributeName: self.font]).height ?? 0
// Dividing the smallestWidthOrHeight by 2 gives us the radius for the circle.
return (smallestWidthOrHeight/2) - heightOfFont + 5
}
}
Example of Usage on Storyboard
Changes I made
I removed parameters that I could get straight from the label now.
I'm admittedly not the smartest in Trigonometry and have forgotten a lot at my age so I included all the relevant definitions so I could start to understand #Grimxn brilliancy.
The angle and clockwise settings are now properties you can adjust in Attributes Inspector.
I create the radius from the size of the label now.
Put some of the comments in standard format on functions, you know, so you get that popup that comes up with you OPTION + CLICK functions.
Problems I have seen
I encourage you to edit the above to improve it.
I don't know why but sometimes the label kept rendering over other controls even though it was behind them in the document outline.
Always the same implementation but adjusted for Swift 4
import UIKit
#IBDesignable
class CircularLabel: UILabel {
// *******************************************************
// DEFINITIONS (Because I'm not brilliant and I'll forget most this tomorrow.)
// Radius: A straight line from the center to the circumference of a circle.
// Circumference: The distance around the edge (outer line) the circle.
// Arc: A part of the circumference of a circle. Like a length or section of the circumference.
// Theta: A label or name that represents an angle.
// Subtend: A letter has a width. If you put the letter on the circumference, the letter's width
// gives you an arc. So now that you have an arc (a length on the circumference) you can
// use that to get an angle. You get an angle when you draw a line from the center of the
// circle to each end point of your arc. So "subtend" means to get an angle from an arc.
// Chord: A line segment connecting two points on a curve. If you have an arc then there is a
// start point and an end point. If you draw a straight line from start point to end point
// then you have a "chord".
// sin: (Super simple/incomplete definition) Or "sine" takes an angle in degrees and gives you a number.
// asin: Or "asine" takes a number and gives you an angle in degrees. Opposite of sine.
// More complete definition: http://www.mathsisfun.com/sine-cosine-tangent.html
// cosine: Also takes an angle in degrees and gives you another number from using the two radiuses (radii).
// *******************************************************
#IBInspectable var angle: CGFloat = 1.6
#IBInspectable var clockwise: Bool = true
override func draw(_ rect: CGRect) {
centreArcPerpendicular()
}
/**
This draws the self.text around an arc of radius r,
with the text centred at polar angle theta
*/
func centreArcPerpendicular() {
guard let context = UIGraphicsGetCurrentContext() else { return }
let string = text ?? ""
let size = bounds.size
context.translateBy(x: size.width / 2, y: size.height / 2)
let radius = getRadiusForLabel()
let l = string.count
let attributes = [NSAttributedStringKey.font : self.font!]
let characters: [String] = string.map { String($0) } // An array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: radius)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat.pi/2 : CGFloat.pi/2
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = angle - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centre with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: radius, angle: thetaI, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
/**
This draws the String str centred at the position
specified by the polar coordinates (r, theta)
i.e. the x= r * cos(theta) y= r * sin(theta)
and rotated by the angle slantAngle
*/
func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, slantAngle: CGFloat) {
// Set the text attributes
let attributes : [NSAttributedStringKey : Any] = [
NSAttributedStringKey.foregroundColor: textColor!,
NSAttributedStringKey.font: font!
]
// Save the context
context.saveGState()
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(withAttributes: attributes)
// Move the origin by half the size of the text
context.translateBy(x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
func getRadiusForLabel() -> CGFloat {
// Imagine the bounds of this label will have a circle inside it.
// The circle will be as big as the smallest width or height of this label.
// But we need to fit the size of the font on the circle so make the circle a little
// smaller so the text does not get drawn outside the bounds of the circle.
let smallestWidthOrHeight = min(bounds.size.height, bounds.size.width)
let heightOfFont = text?.size(withAttributes: [NSAttributedStringKey.font: self.font]).height ?? 0
// Dividing the smallestWidthOrHeight by 2 gives us the radius for the circle.
return (smallestWidthOrHeight/2) - heightOfFont + 5
}
}
#IBDesignable For UILabel on Circular Path for Swift 2
Big thanks to both #Grimxn and #mark-moeykens for the absolutely killer work. I've done a small refactor on Mark's work so I could use it in a project that hasn't taken the time to update to Swift 3. Wanted to share, since the previous posts were so helpful.
Swift 2 Code for Custom UILabel
import UIKit
#IBDesignable
class ArcUILabel: UILabel
{
// *******************************************************
// DEFINITIONS (Because I'm not brilliant and I'll forget most this tomorrow.)
// Radius: A straight line from the center to the circumference of a circle.
// Circumference: The distance around the edge (outer line) the circle.
// Arc: A part of the circumference of a circle. Like a length or section of the circumference.
// Theta: A label or name that represents an angle.
// Subtend: A letter has a width. If you put the letter on the circumference, the letter's width
// gives you an arc. So now that you have an arc (a length on the circumference) you can
// use that to get an angle. You get an angle when you draw a line from the center of the
// circle to each end point of your arc. So "subtend" means to get an angle from an arc.
// Chord: A line segment connecting two points on a curve. If you have an arc then there is a
// start point and an end point. If you draw a straight line from start point to end point
// then you have a "chord".
// sin: (Super simple/incomplete definition) Or "sine" takes an angle in degrees and gives you a number.
// asin: Or "asine" takes a number and gives you an angle in degrees. Opposite of sine.
// More complete definition: http://www.mathsisfun.com/sine-cosine-tangent.html
// cosine: Also takes an angle in degrees and gives you another number from using the two radiuses (radii).
// *******************************************************
#IBInspectable var angle: CGFloat = 1.6
#IBInspectable var clockwise: Bool = true
override func drawRect(rect: CGRect)
{
centreArcPerpendicular()
}
/**
This draws the self.text around an arc of radius r,
with the text centred at polar angle theta
*/
func centreArcPerpendicular() {
guard let context = UIGraphicsGetCurrentContext() else { return }
let str = self.text ?? ""
let size = self.bounds.size
CGContextTranslateCTM(context, size.width / 2, size.height / 2)
let radius = getRadiusForLabel()
let l = str.characters.count
let attributes: [String : AnyObject] = [NSFontAttributeName: self.font]
let characters: [String] = str.characters.map { String($0) } // An array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
arcs += [chordToArc(characters[i].sizeWithAttributes(attributes).width, radius: radius)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat(M_PI_2) : CGFloat(M_PI_2)
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = angle - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centre with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: radius, angle: thetaI, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
/**
This draws the String str centred at the position
specified by the polar coordinates (r, theta)
i.e. the x= r * cos(theta) y= r * sin(theta)
and rotated by the angle slantAngle
*/
func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, slantAngle: CGFloat) {
// Set the text attributes
let attributes = [NSForegroundColorAttributeName: self.textColor,
NSFontAttributeName: self.font] as [String : AnyObject]
// Save the context
CGContextSaveGState(context)
// Move the origin to the centre of the text (negating the y-axis manually)
CGContextTranslateCTM(context, r * cos(theta), -(r * sin(theta)))
// Rotate the coordinate system
CGContextRotateCTM(context, -slantAngle)
// Calculate the width of the text
let offset: CGSize = str.sizeWithAttributes(attributes)
// Move the origin by half the size of the text
CGContextTranslateCTM(context, -offset.width / 2, -offset.height / 2)
// Draw the text
let txtStr = NSString(string: str)
txtStr.drawAtPoint(CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
CGContextRestoreGState(context)
}
func getRadiusForLabel() -> CGFloat {
// Imagine the bounds of this label will have a circle inside it.
// The circle will be as big as the smallest width or height of this label.
// But we need to fit the size of the font on the circle so make the circle a little
// smaller so the text does not get drawn outside the bounds of the circle.
let smallestWidthOrHeight = min(self.bounds.size.height, self.bounds.size.width)
let heightOfFont = self.text?.sizeWithAttributes([NSFontAttributeName: self.font]).height ?? 0
// Dividing the smallestWidthOrHeight by 2 gives us the radius for the circle.
return (smallestWidthOrHeight/2) - heightOfFont + 5
}
}
A C#-version based on the code from Grimxm:
private void CenterArcPerpendicular(string text, CGContext context, float radius, double angle, UIColor textColor, UIFont font, bool isClockwise) {
var characters = text.ToCharArray();
var arcs = new List<float>() { };
float totalArc = 0;
for (var i = 0; i < characters.Length; i++)
{
var character = new NSString(new string(new char[] { characters[i] }));
var charSize = character.StringSize(font);
var arc = ChordToArc((float)charSize.Width, radius);
arcs.Add(arc);
totalArc += arc;
}
var direction = isClockwise ? -1 : 1;
var slantCorrection = (float)(isClockwise ? -(Math.PI / 2) : (Math.PI / 2));
var thetaI = angle - (direction * (totalArc / 2));
for (var i = 0; i < characters.Length; i++)
{
var character = new NSString(new string(new char[] { characters[i] }));
thetaI += direction * arcs[i] / 2;
CenterText(character, context: context, radius: radius, angle: thetaI, textColor: textColor, font: font, slantAngle: thetaI + slantCorrection);
thetaI += direction * arcs[i] / 2;
}
}
private float ChordToArc(float chord, float radius) {
return (float)(2 * Math.Asin(chord / (2 * radius)));
}
private void CenterText(NSString text, CGContext context, float radius, double angle, UIColor textColor, UIFont font, double slantAngle)
{
var attributes = new UIStringAttributes { Font = font, ForegroundColor = textColor };
context.SaveState();
context.ScaleCTM(1, -1);
var dX = radius * Math.Cos(angle);
var dY = -(radius * Math.Sin(angle));
context.TranslateCTM((nfloat)dX, (nfloat)dY);
context.RotateCTM(-(nfloat)slantAngle);
var offset = text.StringSize(font);
context.TranslateCTM(-offset.Width / 2, -offset.Height / 2);
text.DrawString(CGPoint.Empty, attributes);
context.RestoreState();
}
For use in a Xamarin iOS app.
Update code to Swift 5
import Foundation
import UIKit
#IBDesignable
class UILabelX: UILabel {
#IBInspectable var angle: CGFloat = 1.6
#IBInspectable var clockwise: Bool = true
override func draw(_ rect: CGRect) {
centreArcPerpendicular()
}
/**
This draws the self.text around an arc of radius r,
with the text centred at polar angle theta
*/
func centreArcPerpendicular() {
guard let context = UIGraphicsGetCurrentContext() else { return }
let str = self.text ?? ""
let size = self.bounds.size
context.translateBy(x: size.width / 2, y: size.height / 2)
let radius = getRadiusForLabel()
let l = str.count
// let attributes: [String : Any] = [NSAttributedString.Key: self.font]
let attributes : [NSAttributedString.Key : Any] = [.font : self.font]
let characters: [String] = str.map { String($0) } // An array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
// arcs = [chordToArc(characters[i].widthOfString(usingFont: self.font), radius: radius)]
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: radius)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat(Double.pi/2) : CGFloat(Double.pi/2)
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = angle - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centre with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: radius, angle: thetaI, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
/**
This draws the String str centred at the position
specified by the polar coordinates (r, theta)
i.e. the x= r * cos(theta) y= r * sin(theta)
and rotated by the angle slantAngle
*/
func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, slantAngle: CGFloat) {
// Set the text attributes
let attributes = [NSAttributedString.Key.font: self.font!] as [NSAttributedString.Key : Any]
// Save the context
context.saveGState()
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(withAttributes: attributes)
// Move the origin by half the size of the text
context.translateBy(x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
func getRadiusForLabel() -> CGFloat {
// Imagine the bounds of this label will have a circle inside it.
// The circle will be as big as the smallest width or height of this label.
// But we need to fit the size of the font on the circle so make the circle a little
// smaller so the text does not get drawn outside the bounds of the circle.
let smallestWidthOrHeight = min(self.bounds.size.height, self.bounds.size.width)
let heightOfFont = self.text?.size(withAttributes: [NSAttributedString.Key.font: self.font]).height ?? 0
// Dividing the smallestWidthOrHeight by 2 gives us the radius for the circle.
return (smallestWidthOrHeight/2) - heightOfFont + 5
}
}
Swift 5
Transparent background
Kern option for text
Correct ratio for view
Full playground code
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
// *******************************************************
// Playground code to test
// *******************************************************
let size = CGSize(width: 256, height: 256)
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
let context = UIGraphicsGetCurrentContext()!
// *******************************************************************
// Scale & translate the context to have 0,0
// at the centre of the screen maths convention
// Obviously change your origin to suit...
// *******************************************************************
context.translateBy (x: size.width / 2, y: size.height / 2)
context.scaleBy(x: 1, y: -1)
centreArcPerpendicular(text: "Hello round World",
context: context,
radius: size.height * 0.44,
angle: 0,
colour: UIColor.white,
font: UIFont.systemFont(ofSize: 16),
clockwise: true,
kern: 3)
centreArcPerpendicular(text: "Anticlockwise",
context: context,
radius: size.height * 0.44,
angle: -.pi,
colour: UIColor.white,
font: UIFont.systemFont(ofSize: 16),
clockwise: false,
kern: 3)
centre(text: "Hello center world", context: context, radius: 0, angle: 0 , colour: UIColor.yellow, font: UIFont.systemFont(ofSize: 16), slantAngle: .pi/4, kern: 0)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
func centreArcPerpendicular(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool, kern: CGFloat = 0) {
// *******************************************************
// This draws the String str around an arc of radius r,
// with the text centred at polar angle theta
// *******************************************************
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
return 2 * asin(chord / (2 * radius))
}
let characters: [String] = str.map { String($0) } // An array of single character strings, each character in str
let l = characters.count
let attributes = [NSAttributedString.Key.font: font,
NSAttributedString.Key.kern: kern] as [NSAttributedString.Key : Any]
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: r)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection: CGFloat = clockwise ? -.pi / 2 : .pi / 2
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = theta - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centerText with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection, kern: kern)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func centre(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat, kern: CGFloat) {
// *******************************************************
// This draws the String str centred at the position
// specified by the polar coordinates (r, theta)
// i.e. the x= r * cos(theta) y= r * sin(theta)
// and rotated by the angle slantAngle
// *******************************************************
// Set the text attributes
let attributes = [NSAttributedString.Key.foregroundColor: c,
NSAttributedString.Key.font: font,
NSAttributedString.Key.kern: kern] as [NSAttributedString.Key : Any]
//let attributes = [NSForegroundColorAttributeName: c, NSFontAttributeName: font]
// Save the context
context.saveGState()
// Undo the inversion of the Y-axis (or the text goes backwards!)
context.scaleBy(x: 1, y: -1)
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(withAttributes: attributes)
// Move the origin by half the size of the text
context.translateBy (x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

How to draw a circle diagram looking like a clock face using UIKit

I've been trying to figure this out for too long. With the help of this blog I managed to draw the diagram itself, but it can't show me any data, because it seems like my idea of creating a context array is not possible and I can have only one context per view, is that right? So how can I change the color of each marker individually? I've seen the solution using SpriteKit, but I don't know anything at all about SpriteKit.
func degree2Radian(a:CGFloat)->CGFloat {
let b = CGFloat(M_PI) * a/180
return b
}
override func draw(_ rect: CGRect) {
color.set()
pathForCircleCenteredAtPoint(midPoint: circleCenter, withRadius: circleRadius).stroke()
color = UIColor.white
color.set()
pathForCircleCenteredAtPoint(midPoint: CGPoint(x: bounds.midX, y: bounds.midY), withRadius: circleRadius).fill()
color = UIColor(red: 0.93, green: 0.93, blue: 0.94, alpha: 1)
color.set()
let ctx = UIGraphicsGetCurrentContext()
for i in 0...100 {
secondMarkers(ctx: ctx!, x: circleCenter.x, y: circleCenter.y, radius: circleRadius - 4, sides: 100, color: color)
}
diagramArray[0].strokePath()
}
func degree2radian(a:CGFloat)->CGFloat {
let b = CGFloat(M_PI) * a/180
return b
}
func circleCircumferencePoints(sides:Int,x:CGFloat,y:CGFloat,radius:CGFloat,adjustment:CGFloat=0)->[CGPoint] {
let angle = degree2radian(a: 360/CGFloat(sides))
let cx = x // x origin
let cy = y // y origin
let r = radius // radius of circle
var i = sides
var points = [CGPoint]()
while points.count <= sides {
let xpo = cx - r * cos(angle * CGFloat(i)+degree2radian(a: adjustment))
let ypo = cy - r * sin(angle * CGFloat(i)+degree2radian(a: adjustment))
points.append(CGPoint(x: xpo, y: ypo))
i -= 1;
}
return points
}
func secondMarkers(ctx:CGContext, x:CGFloat, y:CGFloat, radius:CGFloat, sides:Int, color:UIColor) {
// retrieve points
let points = circleCircumferencePoints(sides: sides,x: x,y: y,radius: radius)
// create path
// determine length of marker as a fraction of the total radius
var divider:CGFloat = 1/16
//for p in points {
let path = CGMutablePath()
divider = 1/10
let xn = points[counter].x + divider * (x-points[counter].x)
let yn = points[counter].y + divider * (y-points[counter].y)
// build path
path.move(to: CGPoint(x: points[counter].x, y: points[counter].y))
//path, nil, p.x, p.y)
path.addLine(to: CGPoint(x: xn, y: yn))
//CGPathAddLineToPoint(path, nil, xn, yn)
path.closeSubpath()
// add path to context
ctx.addPath(path)
ctx.setStrokeColor(color.cgColor)
ctx.setLineWidth(2.0)
//ctx.strokePath()
diagramArray.append(ctx)
counter += 1
//}
// set path color
}
So basically I'm trying to append context for each marker to an array, but when I draw one element of this array, it draws the whole diagram. This is what I need to achieve.
You shouldn't need to create more than one CGContext - you should just be reusing the same one to draw all graphics. Also, your method to calculate the secondMarkers seems unnecessarily complex. I believe this does what you want:
private func drawTicks(context: CGContext, tickCount: Int, center: CGPoint, startRadius: CGFloat, endRadius: CGFloat, ticksToColor: Int) {
for i in 0 ... tickCount {
let color: UIColor = i < ticksToColor ? .blue : .lightGray
context.setStrokeColor(color.cgColor)
let angle = .pi - degree2Radian(a: (CGFloat(360.0) / CGFloat(tickCount)) * CGFloat(i))
let path = CGMutablePath()
path.move(to: circleCircumferencePoint(center: center, angle: angle, radius: startRadius))
path.addLine(to: circleCircumferencePoint(center: center, angle: angle, radius: endRadius))
context.addPath(path)
context.strokePath()
}
}
private func circleCircumferencePoint(center: CGPoint, angle: CGFloat, radius: CGFloat) -> CGPoint {
return CGPoint(x: radius * sin(angle) + center.x, y: radius * cos(angle) + center.y)
}

Draw text along circular path in Swift for iOS

I am looking for some up to date help/hints on how to draw simple single line strings around the edge of a circle using Swift2 for iOS9. I see quite dated examples involving old ObjC fragments, and oft limited to OS X only. Is this even possible in iOS within a custom UIView subclass's drawRect() method?
I was going to say "What have you tried?", but it's Friday afternoon and I got off work early, so I took the opportunity to translate my old ObjC code. Here it is, suitable for Playground. It should be trivial to put it in your UIView.
Swift 2
See below for Swift 3 & Swift 4 updates...
import UIKit
func centreArcPerpendicularText(str: String, context: CGContextRef, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool){
// *******************************************************
// This draws the String str around an arc of radius r,
// with the text centred at polar angle theta
// *******************************************************
let l = str.characters.count
let attributes = [NSFontAttributeName: font]
var characters: [String] = [] // This will be an array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
characters += [String(str[str.startIndex.advancedBy(i)])]
arcs += [chordToArc(characters[i].sizeWithAttributes(attributes).width, radius: r)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat(M_PI_2) : CGFloat(M_PI_2)
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = theta - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centerText with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centreText(characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
func centreText(str: String, context: CGContextRef, radius r:CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat) {
// *******************************************************
// This draws the String str centred at the position
// specified by the polar coordinates (r, theta)
// i.e. the x= r * cos(theta) y= r * sin(theta)
// and rotated by the angle slantAngle
// *******************************************************
// Set the text attributes
let attributes = [NSForegroundColorAttributeName: c,
NSFontAttributeName: font]
// Save the context
CGContextSaveGState(context)
// Undo the inversion of the Y-axis (or the text goes backwards!)
CGContextScaleCTM(context, 1, -1)
// Move the origin to the centre of the text (negating the y-axis manually)
CGContextTranslateCTM(context, r * cos(theta), -(r * sin(theta)))
// Rotate the coordinate system
CGContextRotateCTM(context, -slantAngle)
// Calculate the width of the text
let offset = str.sizeWithAttributes(attributes)
// Move the origin by half the size of the text
CGContextTranslateCTM (context, -offset.width / 2, -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.drawAtPoint(CGPointZero, withAttributes: attributes)
// Restore the context
CGContextRestoreGState(context)
}
// *******************************************************
// Playground code to test
// *******************************************************
let size = CGSize(width: 256, height: 256)
UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
let context = UIGraphicsGetCurrentContext()!
// *******************************************************************
// Scale & translate the context to have 0,0
// at the centre of the screen maths convention
// Obviously change your origin to suit...
// *******************************************************************
CGContextTranslateCTM (context, size.width / 2, size.height / 2)
CGContextScaleCTM (context, 1, -1)
centreArcPerpendicularText("Hello round world", context: context, radius: 100, angle: 0, colour: UIColor.redColor(), font: UIFont.systemFontOfSize(16), clockwise: true)
centreArcPerpendicularText("Anticlockwise", context: context, radius: 100, angle: CGFloat(-M_PI_2), colour: UIColor.redColor(), font: UIFont.systemFontOfSize(16), clockwise: false)
centreText("Hello flat world", context: context, radius: 0, angle: 0 , colour: UIColor.yellowColor(), font: UIFont.systemFontOfSize(16), slantAngle: CGFloat(M_PI_4))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
Output is:
Update
Added clockwise / anticlockwise & straight example.
Update Swift 3
func centreArcPerpendicular(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool){
// *******************************************************
// This draws the String str around an arc of radius r,
// with the text centred at polar angle theta
// *******************************************************
let l = str.characters.count
let attributes = [NSFontAttributeName: font]
let characters: [String] = str.characters.map { String($0) } // An array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
arcs += [chordToArc(characters[i].size(attributes: attributes).width, radius: r)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat(M_PI_2) : CGFloat(M_PI_2)
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = theta - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centerText with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat) {
// *******************************************************
// This draws the String str centred at the position
// specified by the polar coordinates (r, theta)
// i.e. the x= r * cos(theta) y= r * sin(theta)
// and rotated by the angle slantAngle
// *******************************************************
// Set the text attributes
let attributes = [NSForegroundColorAttributeName: c,
NSFontAttributeName: font]
// Save the context
context.saveGState()
// Undo the inversion of the Y-axis (or the text goes backwards!)
context.scaleBy(x: 1, y: -1)
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(attributes: attributes)
// Move the origin by half the size of the text
context.translateBy (x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
// *******************************************************
// Playground code to test
// *******************************************************
let size = CGSize(width: 256, height: 256)
UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
let context = UIGraphicsGetCurrentContext()!
// *******************************************************************
// Scale & translate the context to have 0,0
// at the centre of the screen maths convention
// Obviously change your origin to suit...
// *******************************************************************
context.translateBy (x: size.width / 2, y: size.height / 2)
context.scaleBy (x: 1, y: -1)
centreArcPerpendicular(text: "Hello round world", context: context, radius: 100, angle: 0, colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: true)
centreArcPerpendicular(text: "Anticlockwise", context: context, radius: 100, angle: CGFloat(-M_PI_2), colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: false)
centre(text: "Hello flat world", context: context, radius: 0, angle: 0 , colour: UIColor.yellow, font: UIFont.systemFont(ofSize: 16), slantAngle: CGFloat(M_PI_4))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
Swift 4
Yet again, minor changes, this time fixing the deprecation of M_PI, String's abandonment of .characters, the parameter label change in .size(withAttributes..., and the change in text attributes to the NSAttributedStringKey enum...
import UIKit
func centreArcPerpendicular(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool){
// *******************************************************
// This draws the String str around an arc of radius r,
// with the text centred at polar angle theta
// *******************************************************
let characters: [String] = str.map { String($0) } // An array of single character strings, each character in str
let l = characters.count
let attributes = [NSAttributedStringKey.font: font]
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: r)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection: CGFloat = clockwise ? -.pi / 2 : .pi / 2
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = theta - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centerText with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
func centre(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat) {
// *******************************************************
// This draws the String str centred at the position
// specified by the polar coordinates (r, theta)
// i.e. the x= r * cos(theta) y= r * sin(theta)
// and rotated by the angle slantAngle
// *******************************************************
// Set the text attributes
let attributes = [NSAttributedStringKey.foregroundColor: c, NSAttributedStringKey.font: font]
//let attributes = [NSForegroundColorAttributeName: c, NSFontAttributeName: font]
// Save the context
context.saveGState()
// Undo the inversion of the Y-axis (or the text goes backwards!)
context.scaleBy(x: 1, y: -1)
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(withAttributes: attributes)
// Move the origin by half the size of the text
context.translateBy (x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
// *******************************************************
// Playground code to test
// *******************************************************
let size = CGSize(width: 256, height: 256)
UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
let context = UIGraphicsGetCurrentContext()!
// *******************************************************************
// Scale & translate the context to have 0,0
// at the centre of the screen maths convention
// Obviously change your origin to suit...
// *******************************************************************
context.translateBy (x: size.width / 2, y: size.height / 2)
context.scaleBy(x: 1, y: -1)
centreArcPerpendicular(text: "Hello round 🌏 world", context: context, radius: 100, angle: 0, colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: true)
centreArcPerpendicular(text: "Anticlockwise", context: context, radius: 100, angle: CGFloat(-M_PI_2), colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: false)
centre(text: "Hello flat world", context: context, radius: 0, angle: 0 , colour: UIColor.yellow, font: UIFont.systemFont(ofSize: 16), slantAngle: .pi / 4)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
Update to show use in UIView
Commentator #RitvikUpadhyaya asks how to do this in a UIView - obvious to old hands, but not perhaps to beginners. The trick is to get the right context using UIGraphicsGetCurrentContext without calling UIGraphicsBeginImageContextWithOptions (which overrides the UIView's context as the current context) - therefore your UIView should look like this:
class MyView: UIView {
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let size = self.bounds.size
context.translateBy (x: size.width / 2, y: size.height / 2)
context.scaleBy (x: 1, y: -1)
centreArcPerpendicular(text: "Hello round world", context: context, radius: 100, angle: 0, colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: true)
centreArcPerpendicular(text: "Anticlockwise", context: context, radius: 100, angle: CGFloat(-M_PI_2), colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: false)
centre(text: "Hello flat world", context: context, radius: 0, angle: 0 , colour: UIColor.yellow, font: UIFont.systemFont(ofSize: 16), slantAngle: CGFloat(M_PI_4))
}
}
#IBDesignable For UILabel on Circular Path
First of all, I think we can all agree that #Grimxn is THE MAN! His solution kicks butt. I took his work and refactored it into a custom UILabel control that you can set and edit on the Storyboard. If you guys watch my videos you know how much I love to do this stuff! 😀
Swift 3 Code for Custom UILabel
import UIKit
#IBDesignable
class UILabelX: UILabel {
// *******************************************************
// DEFINITIONS (Because I'm not brilliant and I'll forget most this tomorrow.)
// Radius: A straight line from the center to the circumference of a circle.
// Circumference: The distance around the edge (outer line) the circle.
// Arc: A part of the circumference of a circle. Like a length or section of the circumference.
// Theta: A label or name that represents an angle.
// Subtend: A letter has a width. If you put the letter on the circumference, the letter's width
// gives you an arc. So now that you have an arc (a length on the circumference) you can
// use that to get an angle. You get an angle when you draw a line from the center of the
// circle to each end point of your arc. So "subtend" means to get an angle from an arc.
// Chord: A line segment connecting two points on a curve. If you have an arc then there is a
// start point and an end point. If you draw a straight line from start point to end point
// then you have a "chord".
// sin: (Super simple/incomplete definition) Or "sine" takes an angle in degrees and gives you a number.
// asin: Or "asine" takes a number and gives you an angle in degrees. Opposite of sine.
// More complete definition: http://www.mathsisfun.com/sine-cosine-tangent.html
// cosine: Also takes an angle in degrees and gives you another number from using the two radiuses (radii).
// *******************************************************
#IBInspectable var angle: CGFloat = 1.6
#IBInspectable var clockwise: Bool = true
override func draw(_ rect: CGRect) {
centreArcPerpendicular()
}
/**
This draws the self.text around an arc of radius r,
with the text centred at polar angle theta
*/
func centreArcPerpendicular() {
guard let context = UIGraphicsGetCurrentContext() else { return }
let str = self.text ?? ""
let size = self.bounds.size
context.translateBy(x: size.width / 2, y: size.height / 2)
let radius = getRadiusForLabel()
let l = str.characters.count
let attributes: [String : Any] = [NSFontAttributeName: self.font]
let characters: [String] = str.characters.map { String($0) } // An array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
arcs += [chordToArc(characters[i].size(attributes: attributes).width, radius: radius)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat(M_PI_2) : CGFloat(M_PI_2)
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = angle - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centre with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: radius, angle: thetaI, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
/**
This draws the String str centred at the position
specified by the polar coordinates (r, theta)
i.e. the x= r * cos(theta) y= r * sin(theta)
and rotated by the angle slantAngle
*/
func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, slantAngle: CGFloat) {
// Set the text attributes
let attributes = [NSForegroundColorAttributeName: self.textColor,
NSFontAttributeName: self.font] as [String : Any]
// Save the context
context.saveGState()
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(attributes: attributes)
// Move the origin by half the size of the text
context.translateBy(x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
func getRadiusForLabel() -> CGFloat {
// Imagine the bounds of this label will have a circle inside it.
// The circle will be as big as the smallest width or height of this label.
// But we need to fit the size of the font on the circle so make the circle a little
// smaller so the text does not get drawn outside the bounds of the circle.
let smallestWidthOrHeight = min(self.bounds.size.height, self.bounds.size.width)
let heightOfFont = self.text?.size(attributes: [NSFontAttributeName: self.font]).height ?? 0
// Dividing the smallestWidthOrHeight by 2 gives us the radius for the circle.
return (smallestWidthOrHeight/2) - heightOfFont + 5
}
}
Example of Usage on Storyboard
Changes I made
I removed parameters that I could get straight from the label now.
I'm admittedly not the smartest in Trigonometry and have forgotten a lot at my age so I included all the relevant definitions so I could start to understand #Grimxn brilliancy.
The angle and clockwise settings are now properties you can adjust in Attributes Inspector.
I create the radius from the size of the label now.
Put some of the comments in standard format on functions, you know, so you get that popup that comes up with you OPTION + CLICK functions.
Problems I have seen
I encourage you to edit the above to improve it.
I don't know why but sometimes the label kept rendering over other controls even though it was behind them in the document outline.
Always the same implementation but adjusted for Swift 4
import UIKit
#IBDesignable
class CircularLabel: UILabel {
// *******************************************************
// DEFINITIONS (Because I'm not brilliant and I'll forget most this tomorrow.)
// Radius: A straight line from the center to the circumference of a circle.
// Circumference: The distance around the edge (outer line) the circle.
// Arc: A part of the circumference of a circle. Like a length or section of the circumference.
// Theta: A label or name that represents an angle.
// Subtend: A letter has a width. If you put the letter on the circumference, the letter's width
// gives you an arc. So now that you have an arc (a length on the circumference) you can
// use that to get an angle. You get an angle when you draw a line from the center of the
// circle to each end point of your arc. So "subtend" means to get an angle from an arc.
// Chord: A line segment connecting two points on a curve. If you have an arc then there is a
// start point and an end point. If you draw a straight line from start point to end point
// then you have a "chord".
// sin: (Super simple/incomplete definition) Or "sine" takes an angle in degrees and gives you a number.
// asin: Or "asine" takes a number and gives you an angle in degrees. Opposite of sine.
// More complete definition: http://www.mathsisfun.com/sine-cosine-tangent.html
// cosine: Also takes an angle in degrees and gives you another number from using the two radiuses (radii).
// *******************************************************
#IBInspectable var angle: CGFloat = 1.6
#IBInspectable var clockwise: Bool = true
override func draw(_ rect: CGRect) {
centreArcPerpendicular()
}
/**
This draws the self.text around an arc of radius r,
with the text centred at polar angle theta
*/
func centreArcPerpendicular() {
guard let context = UIGraphicsGetCurrentContext() else { return }
let string = text ?? ""
let size = bounds.size
context.translateBy(x: size.width / 2, y: size.height / 2)
let radius = getRadiusForLabel()
let l = string.count
let attributes = [NSAttributedStringKey.font : self.font!]
let characters: [String] = string.map { String($0) } // An array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: radius)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat.pi/2 : CGFloat.pi/2
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = angle - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centre with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: radius, angle: thetaI, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
/**
This draws the String str centred at the position
specified by the polar coordinates (r, theta)
i.e. the x= r * cos(theta) y= r * sin(theta)
and rotated by the angle slantAngle
*/
func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, slantAngle: CGFloat) {
// Set the text attributes
let attributes : [NSAttributedStringKey : Any] = [
NSAttributedStringKey.foregroundColor: textColor!,
NSAttributedStringKey.font: font!
]
// Save the context
context.saveGState()
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(withAttributes: attributes)
// Move the origin by half the size of the text
context.translateBy(x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
func getRadiusForLabel() -> CGFloat {
// Imagine the bounds of this label will have a circle inside it.
// The circle will be as big as the smallest width or height of this label.
// But we need to fit the size of the font on the circle so make the circle a little
// smaller so the text does not get drawn outside the bounds of the circle.
let smallestWidthOrHeight = min(bounds.size.height, bounds.size.width)
let heightOfFont = text?.size(withAttributes: [NSAttributedStringKey.font: self.font]).height ?? 0
// Dividing the smallestWidthOrHeight by 2 gives us the radius for the circle.
return (smallestWidthOrHeight/2) - heightOfFont + 5
}
}
#IBDesignable For UILabel on Circular Path for Swift 2
Big thanks to both #Grimxn and #mark-moeykens for the absolutely killer work. I've done a small refactor on Mark's work so I could use it in a project that hasn't taken the time to update to Swift 3. Wanted to share, since the previous posts were so helpful.
Swift 2 Code for Custom UILabel
import UIKit
#IBDesignable
class ArcUILabel: UILabel
{
// *******************************************************
// DEFINITIONS (Because I'm not brilliant and I'll forget most this tomorrow.)
// Radius: A straight line from the center to the circumference of a circle.
// Circumference: The distance around the edge (outer line) the circle.
// Arc: A part of the circumference of a circle. Like a length or section of the circumference.
// Theta: A label or name that represents an angle.
// Subtend: A letter has a width. If you put the letter on the circumference, the letter's width
// gives you an arc. So now that you have an arc (a length on the circumference) you can
// use that to get an angle. You get an angle when you draw a line from the center of the
// circle to each end point of your arc. So "subtend" means to get an angle from an arc.
// Chord: A line segment connecting two points on a curve. If you have an arc then there is a
// start point and an end point. If you draw a straight line from start point to end point
// then you have a "chord".
// sin: (Super simple/incomplete definition) Or "sine" takes an angle in degrees and gives you a number.
// asin: Or "asine" takes a number and gives you an angle in degrees. Opposite of sine.
// More complete definition: http://www.mathsisfun.com/sine-cosine-tangent.html
// cosine: Also takes an angle in degrees and gives you another number from using the two radiuses (radii).
// *******************************************************
#IBInspectable var angle: CGFloat = 1.6
#IBInspectable var clockwise: Bool = true
override func drawRect(rect: CGRect)
{
centreArcPerpendicular()
}
/**
This draws the self.text around an arc of radius r,
with the text centred at polar angle theta
*/
func centreArcPerpendicular() {
guard let context = UIGraphicsGetCurrentContext() else { return }
let str = self.text ?? ""
let size = self.bounds.size
CGContextTranslateCTM(context, size.width / 2, size.height / 2)
let radius = getRadiusForLabel()
let l = str.characters.count
let attributes: [String : AnyObject] = [NSFontAttributeName: self.font]
let characters: [String] = str.characters.map { String($0) } // An array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
arcs += [chordToArc(characters[i].sizeWithAttributes(attributes).width, radius: radius)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat(M_PI_2) : CGFloat(M_PI_2)
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = angle - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centre with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: radius, angle: thetaI, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
/**
This draws the String str centred at the position
specified by the polar coordinates (r, theta)
i.e. the x= r * cos(theta) y= r * sin(theta)
and rotated by the angle slantAngle
*/
func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, slantAngle: CGFloat) {
// Set the text attributes
let attributes = [NSForegroundColorAttributeName: self.textColor,
NSFontAttributeName: self.font] as [String : AnyObject]
// Save the context
CGContextSaveGState(context)
// Move the origin to the centre of the text (negating the y-axis manually)
CGContextTranslateCTM(context, r * cos(theta), -(r * sin(theta)))
// Rotate the coordinate system
CGContextRotateCTM(context, -slantAngle)
// Calculate the width of the text
let offset: CGSize = str.sizeWithAttributes(attributes)
// Move the origin by half the size of the text
CGContextTranslateCTM(context, -offset.width / 2, -offset.height / 2)
// Draw the text
let txtStr = NSString(string: str)
txtStr.drawAtPoint(CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
CGContextRestoreGState(context)
}
func getRadiusForLabel() -> CGFloat {
// Imagine the bounds of this label will have a circle inside it.
// The circle will be as big as the smallest width or height of this label.
// But we need to fit the size of the font on the circle so make the circle a little
// smaller so the text does not get drawn outside the bounds of the circle.
let smallestWidthOrHeight = min(self.bounds.size.height, self.bounds.size.width)
let heightOfFont = self.text?.sizeWithAttributes([NSFontAttributeName: self.font]).height ?? 0
// Dividing the smallestWidthOrHeight by 2 gives us the radius for the circle.
return (smallestWidthOrHeight/2) - heightOfFont + 5
}
}
A C#-version based on the code from Grimxm:
private void CenterArcPerpendicular(string text, CGContext context, float radius, double angle, UIColor textColor, UIFont font, bool isClockwise) {
var characters = text.ToCharArray();
var arcs = new List<float>() { };
float totalArc = 0;
for (var i = 0; i < characters.Length; i++)
{
var character = new NSString(new string(new char[] { characters[i] }));
var charSize = character.StringSize(font);
var arc = ChordToArc((float)charSize.Width, radius);
arcs.Add(arc);
totalArc += arc;
}
var direction = isClockwise ? -1 : 1;
var slantCorrection = (float)(isClockwise ? -(Math.PI / 2) : (Math.PI / 2));
var thetaI = angle - (direction * (totalArc / 2));
for (var i = 0; i < characters.Length; i++)
{
var character = new NSString(new string(new char[] { characters[i] }));
thetaI += direction * arcs[i] / 2;
CenterText(character, context: context, radius: radius, angle: thetaI, textColor: textColor, font: font, slantAngle: thetaI + slantCorrection);
thetaI += direction * arcs[i] / 2;
}
}
private float ChordToArc(float chord, float radius) {
return (float)(2 * Math.Asin(chord / (2 * radius)));
}
private void CenterText(NSString text, CGContext context, float radius, double angle, UIColor textColor, UIFont font, double slantAngle)
{
var attributes = new UIStringAttributes { Font = font, ForegroundColor = textColor };
context.SaveState();
context.ScaleCTM(1, -1);
var dX = radius * Math.Cos(angle);
var dY = -(radius * Math.Sin(angle));
context.TranslateCTM((nfloat)dX, (nfloat)dY);
context.RotateCTM(-(nfloat)slantAngle);
var offset = text.StringSize(font);
context.TranslateCTM(-offset.Width / 2, -offset.Height / 2);
text.DrawString(CGPoint.Empty, attributes);
context.RestoreState();
}
For use in a Xamarin iOS app.
Update code to Swift 5
import Foundation
import UIKit
#IBDesignable
class UILabelX: UILabel {
#IBInspectable var angle: CGFloat = 1.6
#IBInspectable var clockwise: Bool = true
override func draw(_ rect: CGRect) {
centreArcPerpendicular()
}
/**
This draws the self.text around an arc of radius r,
with the text centred at polar angle theta
*/
func centreArcPerpendicular() {
guard let context = UIGraphicsGetCurrentContext() else { return }
let str = self.text ?? ""
let size = self.bounds.size
context.translateBy(x: size.width / 2, y: size.height / 2)
let radius = getRadiusForLabel()
let l = str.count
// let attributes: [String : Any] = [NSAttributedString.Key: self.font]
let attributes : [NSAttributedString.Key : Any] = [.font : self.font]
let characters: [String] = str.map { String($0) } // An array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
// arcs = [chordToArc(characters[i].widthOfString(usingFont: self.font), radius: radius)]
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: radius)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat(Double.pi/2) : CGFloat(Double.pi/2)
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = angle - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centre with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: radius, angle: thetaI, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
/**
This draws the String str centred at the position
specified by the polar coordinates (r, theta)
i.e. the x= r * cos(theta) y= r * sin(theta)
and rotated by the angle slantAngle
*/
func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, slantAngle: CGFloat) {
// Set the text attributes
let attributes = [NSAttributedString.Key.font: self.font!] as [NSAttributedString.Key : Any]
// Save the context
context.saveGState()
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(withAttributes: attributes)
// Move the origin by half the size of the text
context.translateBy(x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
func getRadiusForLabel() -> CGFloat {
// Imagine the bounds of this label will have a circle inside it.
// The circle will be as big as the smallest width or height of this label.
// But we need to fit the size of the font on the circle so make the circle a little
// smaller so the text does not get drawn outside the bounds of the circle.
let smallestWidthOrHeight = min(self.bounds.size.height, self.bounds.size.width)
let heightOfFont = self.text?.size(withAttributes: [NSAttributedString.Key.font: self.font]).height ?? 0
// Dividing the smallestWidthOrHeight by 2 gives us the radius for the circle.
return (smallestWidthOrHeight/2) - heightOfFont + 5
}
}
Swift 5
Transparent background
Kern option for text
Correct ratio for view
Full playground code
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
// *******************************************************
// Playground code to test
// *******************************************************
let size = CGSize(width: 256, height: 256)
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
let context = UIGraphicsGetCurrentContext()!
// *******************************************************************
// Scale & translate the context to have 0,0
// at the centre of the screen maths convention
// Obviously change your origin to suit...
// *******************************************************************
context.translateBy (x: size.width / 2, y: size.height / 2)
context.scaleBy(x: 1, y: -1)
centreArcPerpendicular(text: "Hello round World",
context: context,
radius: size.height * 0.44,
angle: 0,
colour: UIColor.white,
font: UIFont.systemFont(ofSize: 16),
clockwise: true,
kern: 3)
centreArcPerpendicular(text: "Anticlockwise",
context: context,
radius: size.height * 0.44,
angle: -.pi,
colour: UIColor.white,
font: UIFont.systemFont(ofSize: 16),
clockwise: false,
kern: 3)
centre(text: "Hello center world", context: context, radius: 0, angle: 0 , colour: UIColor.yellow, font: UIFont.systemFont(ofSize: 16), slantAngle: .pi/4, kern: 0)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
func centreArcPerpendicular(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool, kern: CGFloat = 0) {
// *******************************************************
// This draws the String str around an arc of radius r,
// with the text centred at polar angle theta
// *******************************************************
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
return 2 * asin(chord / (2 * radius))
}
let characters: [String] = str.map { String($0) } // An array of single character strings, each character in str
let l = characters.count
let attributes = [NSAttributedString.Key.font: font,
NSAttributedString.Key.kern: kern] as [NSAttributedString.Key : Any]
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: r)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection: CGFloat = clockwise ? -.pi / 2 : .pi / 2
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = theta - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centerText with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection, kern: kern)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func centre(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat, kern: CGFloat) {
// *******************************************************
// This draws the String str centred at the position
// specified by the polar coordinates (r, theta)
// i.e. the x= r * cos(theta) y= r * sin(theta)
// and rotated by the angle slantAngle
// *******************************************************
// Set the text attributes
let attributes = [NSAttributedString.Key.foregroundColor: c,
NSAttributedString.Key.font: font,
NSAttributedString.Key.kern: kern] as [NSAttributedString.Key : Any]
//let attributes = [NSForegroundColorAttributeName: c, NSFontAttributeName: font]
// Save the context
context.saveGState()
// Undo the inversion of the Y-axis (or the text goes backwards!)
context.scaleBy(x: 1, y: -1)
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(withAttributes: attributes)
// Move the origin by half the size of the text
context.translateBy (x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

Resources