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