ios swift swift2 drawrect swift3

Dibuje texto a lo largo de una ruta circular en Swift para iOS



swift2 drawrect (4)

Estoy buscando ayuda / sugerencias actualizadas sobre cómo dibujar cadenas simples de una sola línea alrededor del borde de un círculo usando Swift2 para iOS9 . Veo ejemplos bastante anticuados que involucran fragmentos antiguos de ObjC, y a menudo se limitan solo a OS X ¿Es esto posible en iOS dentro del método drawRect() una subclase UIView personalizada?


@IBDesignable para UILabel en la ruta circular para Swift 2

Muchas gracias a @Grimxn y @ mark-moeykens por el trabajo absolutamente asesino. He hecho un pequeño refactor en el trabajo de Mark para poder usarlo en un proyecto que no se ha tomado el tiempo de actualizar a Swift 3. Quería compartir, ya que las publicaciones anteriores fueron muy útiles.

Código Swift 2 para UILabel personalizado

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


@IBDesignable para UILabel en ruta circular

En primer lugar, creo que todos podemos estar de acuerdo en que @Grimxn es EL HOMBRE. Su solución patea el trasero. Tomé su trabajo y lo refactoricé en un control UILabel personalizado que puedes configurar y editar en el Storyboard. Si miran mis videos, ¡saben cuánto me encanta hacer estas cosas! 😀

Código Swift 3 para UILabel personalizado

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

Ejemplo de uso en Storyboard

Cambios que hice

  • Eliminé parámetros que podía obtener directamente de la etiqueta ahora.
  • Es cierto que no soy el más inteligente en Trigonometría y he olvidado mucho a mi edad, así que incluí todas las definiciones relevantes para poder comenzar a comprender la brillantez de @Grimxn.
  • La configuración del ángulo y en sentido horario ahora son propiedades que puede ajustar en el Inspector de atributos.
  • Ahora creo el radio del tamaño de la etiqueta.
  • Ponga algunos de los comentarios en formato estándar sobre las funciones, ya sabe, para que obtenga esa ventana emergente que aparece con sus funciones OPCIÓN + CLIC.

Problemas que he visto

Te animo a editar lo anterior para mejorarlo.

  • No sé por qué, pero a veces la etiqueta seguía renderizándose sobre otros controles aunque estaba detrás de ellos en el esquema del documento.

Iba a decir "¿Qué has intentado?", Pero es viernes por la tarde y salí temprano del trabajo, así que aproveché la oportunidad para traducir mi antiguo código ObjC. Aquí está, adecuado para juegos infantiles. Debería ser trivial ponerlo en su UIView.

Swift 2
Vea a continuación las actualizaciones de Swift 3 y Swift 4 ...

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

Salida es:

Actualización Agregado en sentido horario / antihorario y ejemplo directo.

Actualizar 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
Una vez más, cambios menores, esta vez arreglando la depreciación de M_PI , el abandono de .characters de .characters , el cambio de etiqueta de parámetro en .size(withAttributes... , y el cambio en atributos de texto a la enumeración NSAttributedStringKey ...

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

Actualización para mostrar uso en UIView

El comentarista @RitvikUpadhyaya pregunta cómo hacer esto en una UIView : obvio para los veteranos, pero quizás no para los principiantes. El truco es obtener el contexto correcto usando UIGraphicsGetCurrentContext sin llamar a UIGraphicsBeginImageContextWithOptions (que anula el contexto de UIView como el contexto actual ), por lo tanto, su UIView debería verse así:

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


Siempre la misma implementación pero ajustada para 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 } }