41

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?

pedrouan
  • 12,762
  • 3
  • 58
  • 74
Travis Griggs
  • 21,522
  • 19
  • 91
  • 167

7 Answers7

155

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: Output

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))
    }
}
Grimxn
  • 22,115
  • 10
  • 72
  • 85
  • 26
    Upvoted for including the most amazingly extensive commenting I've seen in 40 years... – matt Sep 25 '15 at 15:31
  • 11
    I'm telling you, when you get to my age, there is /no way/ I could remember what all my code does without it!!! :) – Grimxn Sep 25 '15 at 15:33
  • 2
    This is an absolutely stunning answer! Would you please add the license under which your snippet is available? Corporate friendly MIT, BSD or Apache would be great :) – Rafael Nobre Aug 02 '16 at 05:48
  • 1
    Thank you! My understanding is that the SE terms (http://stackexchange.com/legal) automatically apply Creative Commons Attribution Share Alike license to all material submitted... – Grimxn Aug 02 '16 at 07:33
  • 1
    In case you don't need the text to be centered, but start from the initial coordinate, change the line var thetaI = theta - direction * totalArc / 2 to var thetaI = theta; – JoeGalind Sep 04 '16 at 04:05
  • In Swift 3, UIColor.red() should be UIColor.red – charles.cc.hsu Sep 21 '16 at 04:59
  • This answer looks like it might help me, but how do I apply it to a UILabel. Do I add these functions into a UILabel subclass? How do I get the context of a UILabel? – richc Sep 22 '16 at 13:21
  • 1
    `UILabel` inherits from `UIView`, so subclass `UILabel` then put this in your `override func draw(_ rect: CGRect)`. – Grimxn Sep 22 '16 at 16:30
  • hey, thx for yr help. I have added this to drawRect... but how do I link to the label.text?? let context = UIGraphicsGetCurrentContext() let size = CGSize(width: self.frame.width, height: self.frame.height) 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) – richc Sep 22 '16 at 20:35
  • If you have subclassed UILabel then the text will be self.text. – Grimxn Sep 22 '16 at 21:40
  • ahhh! The text had been translated so out of view. Now sorted. Superb answers! Thx. – richc Sep 23 '16 at 09:33
  • Why don't you add your implementation as a second answer? It's a non-trivial addition! – Grimxn Sep 24 '16 at 08:33
  • 1
    @Grimxn could you tell me how to put this in a view. I tried putting the code in the `draw(_rect: CGRect)` func but the text does not draw! The translation is `context.translateBy (x: size.width / 2, y: size.height / 2)` so that should draw it in the centre! Sorry I dont have much experience with coreanimation so it is not as trivial to me yet! – Ritvik Upadhyaya Jan 09 '17 at 11:23
  • 1
    @RitvikUpadhyaya - the trick is *not* to call `UIGraphicsBeginImageContextWithOptions` but to use the `UIView`'s own context which is current at that point - have updated the answer with example... – Grimxn Jan 09 '17 at 12:31
  • 2
    An epic answer - thank you so much for the work you put in to this! – Mark Brittingham Jan 18 '17 at 18:02
  • @MarkBrittingham - you're very welcome. I wrote it for an astrolabe app years ago, glad you liked it, and very happy to share! – Grimxn Jan 18 '17 at 22:36
  • 1
    @Grimxn, I accepted your challenge to add the UILabel implementation! – Mark Moeykens Mar 12 '17 at 09:03
  • It there any xamarin implementation? – Nininea Oct 17 '17 at 20:59
  • @Nininea - I don't use Xamarin, but the logic of drawing each character shouldn't change, only the `centre:...` function which actually renders each glyph. – Grimxn Oct 18 '17 at 08:30
  • I have tried same code in my project usinf xcode 9.3. The text color not changing. The default text showing black. Any suggestion? – Siva Apr 26 '18 at 07:40
  • @Siva - I have updated it to Swift 4. The handling of Attributes (font colour, etc.) changed in Swift 4... – Grimxn Apr 26 '18 at 10:19
  • @Grimxn Thanks for your reply. I have written like same but label text color not changing here – Siva Apr 26 '18 at 10:48
  • How can I draw a new line character??? I need to draw text in multiline – Raj Aggrawal Apr 08 '19 at 05:57
  • @RajAggrawal - the code already shows you how to get the size of text (it uses it for the width, but it also gets the height). From that it should be trivial to call with different radii for each line. – Grimxn Apr 08 '19 at 09:55
  • Outstanding bit of work! (voted). You forgot to update your UIView code for Swift 4 however. It still references M_PI constants. – Duncan C Oct 01 '19 at 12:19
  • I thought my trig was pretty good, but it took me 15 minutes to figure out what the `chordToArc(_: radius:)` function does, exactly. I forgot that the chord line for a character would be tangent to the circle, and therefore form a right triangle, so I couldn't make sense of the math. – Duncan C Oct 01 '19 at 12:22
  • 1
    Great for Apple Watch OS7 new Extra Large Circular Graphic Complication! Thanks! – Kurt L. Jun 30 '20 at 20:54
  • @Nininea - did you see the additional answer by @wolfgang-schreurs? – Grimxn Dec 28 '20 at 20:58
  • @Grimxn I tried your example. but it is not centering the middle text. Here is video that i have tried. https://drive.google.com/file/d/1MjAaDDrnuXdE61J0cdSF5i4MuwWx61Ob/view?usp=sharing and here is the video which I am trying to acheive https://drive.google.com/file/d/1Qzj1vInxDRz9uFI4mzNw5CR0h49rAC6w/view?usp=sharing – Shahryar Rafique Oct 09 '21 at 08:28
37

@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

Text on Path Usage

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.

Help Text Example

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.
Community
  • 1
  • 1
Mark Moeykens
  • 15,915
  • 6
  • 63
  • 62
  • 2
    Well done! The warning is fixed by changing `let attributes = [NSFontAttributeName: self.font]` to `let attributes: [String : Any] = [NSFontAttributeName: self.font]`. The change in the type of attributes dictionaries came about in Swift 3, IIRC... – Grimxn Mar 12 '17 at 09:50
  • Updated. Thank you! – Mark Moeykens Mar 12 '17 at 16:38
  • Great but how can you set a radius that is not depend on the width/height? I want to just be able to set an arc, and change it dynamically, and even to bring it back to be flat ? https://stackoverflow.com/questions/44967174/curved-text-code-modification – Curnelious Jul 07 '17 at 09:24
  • @MarkMoeykens It doesnt work at all, even I set angle to 1 or 2 it put the text in random place. Why? – Bartłomiej Semańczyk Aug 21 '20 at 06:43
  • @MarkMoeykens what should I do to rotate label -60 degrees left? Let the text starts at 10 o clock, direction clockwise. – Bartłomiej Semańczyk Aug 21 '20 at 07:01
14

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
    }
}
Nicolas Manzini
  • 8,379
  • 6
  • 63
  • 81
  • 1
    One tiny addition I made was to add a "isRTL" boolean for right-to-left languages which just negates the direction but not the slantCorrection (so the text is not "upside down") – GilroyKilroy Jan 11 '18 at 21:19
2
  • 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()

enter image description here

Grimxn
  • 22,115
  • 10
  • 72
  • 85
Yunus T.
  • 569
  • 7
  • 13
1

@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
    }
}
Community
  • 1
  • 1
Jason Wiener
  • 1,851
  • 1
  • 13
  • 13
1

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.

Wolfgang Schreurs
  • 11,779
  • 7
  • 51
  • 92
1

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
}
}
Faiz Ul Hassan
  • 191
  • 1
  • 5