I have a label which I want to make curve up and curve down using slider. I achieve this using following code:
func drawCurvedString(on layer: CALayer, text: NSAttributedString, angle: CGFloat, radius: CGFloat) {
var radAngle = angle.radians
let textSize = text.boundingRect(
with: CGSize(width: .max, height: .max),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil)
.integral
.size
let perimeter: CGFloat = 2 * .pi * radius
let textAngle: CGFloat = textSize.width / perimeter * 2 * .pi
var textRotation: CGFloat = 0
var textDirection: CGFloat = 0
if angle > CGFloat(10).radians, angle < CGFloat(170).radians {
// bottom string
textRotation = 0.5 * .pi
textDirection = -2 * .pi
radAngle += textAngle / 2
} else {
// top string
textRotation = 1.5 * .pi
textDirection = 2 * .pi
radAngle -= textAngle / 2
}
for c in 0..<text.length {
let letter = text.attributedSubstring(from: NSRange(c..<c+1))
let charSize = letter.boundingRect(
with: CGSize(width: .max, height: .max),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil)
.integral
.size
let letterAngle = (charSize.width / perimeter) * textDirection
let x = radius * cos(radAngle + (letterAngle / 2))
let y = radius * sin(radAngle + (letterAngle / 2))
let singleChar = drawText(
on: layer,
text: letter,
frame: CGRect(
x: (layer.frame.size.width / 2) - (charSize.width / 2) + x,
y: (layer.frame.size.height / 2) - (charSize.height / 2) + y,
width: charSize.width,
height: charSize.height))
layer.addSublayer(singleChar)
singleChar.transform = CATransform3DMakeAffineTransform(CGAffineTransform(rotationAngle: radAngle - textRotation))
radAngle += letterAngle
}
}
func drawText(on layer: CALayer, text: NSAttributedString, frame: CGRect) -> CATextLayer {
let textLayer = CATextLayer()
textLayer.frame = frame
textLayer.string = text
textLayer.alignmentMode = kCAAlignmentCenter
textLayer.contentsScale = UIScreen.main.scale
return textLayer
}
But the problem is its add a sublayer of text. All I want to make existing text curve using slider value. There is some code available but that is in objective-c so can anyone help how to achieve this without adding sublayer in swift. Thanks in advance.
Here is a complete example...
View Controller
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let testLabel = UILabelX()
testLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain original image view Top / Leading / Trailing
testLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
testLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
testLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// let's use the image's aspect ratio
testLabel.heightAnchor.constraint(equalToConstant: 300.0),
])
testLabel.backgroundColor = .yellow
testLabel.text = "This is a test of the UILabelX subclass."
}
}
UILabel subclass - from: https://stackoverflow.com/a/69056167/6257435
@IBDesignable
class UILabelX: UILabel {
@IBInspectable var angle: CGFloat = 1.6
@IBInspectable var clockwise: Bool = true
override func draw(_ rect: CGRect) {
centreArcPerpendicular()
}
/**
This draws the self.text around an arc of radius r,
with the text centred at polar angle theta
*/
func centreArcPerpendicular() {
guard let context = UIGraphicsGetCurrentContext() else { return }
let str = self.text ?? ""
let size = self.bounds.size
context.translateBy(x: size.width / 2, y: size.height / 2)
let radius = getRadiusForLabel()
let l = str.count
// let attributes: [String : Any] = [NSAttributedString.Key: self.font]
let attributes : [NSAttributedString.Key : Any] = [.font : self.font]
let characters: [String] = str.map { String($0) } // An array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
// arcs = [chordToArc(characters[i].widthOfString(usingFont: self.font), radius: radius)]
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: radius)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat(Double.pi/2) : CGFloat(Double.pi/2)
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = angle - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centre with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: radius, angle: thetaI, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
/**
This draws the String str centred at the position
specified by the polar coordinates (r, theta)
i.e. the x= r * cos(theta) y= r * sin(theta)
and rotated by the angle slantAngle
*/
func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, slantAngle: CGFloat) {
// Set the text attributes
let attributes = [NSAttributedString.Key.font: self.font!] as [NSAttributedString.Key : Any]
// Save the context
context.saveGState()
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(withAttributes: attributes)
// Move the origin by half the size of the text
context.translateBy(x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
func getRadiusForLabel() -> CGFloat {
// Imagine the bounds of this label will have a circle inside it.
// The circle will be as big as the smallest width or height of this label.
// But we need to fit the size of the font on the circle so make the circle a little
// smaller so the text does not get drawn outside the bounds of the circle.
let smallestWidthOrHeight = min(self.bounds.size.height, self.bounds.size.width)
let heightOfFont = self.text?.size(withAttributes: [NSAttributedString.Key.font: self.font]).height ?? 0
// Dividing the smallestWidthOrHeight by 2 gives us the radius for the circle.
return (smallestWidthOrHeight/2) - heightOfFont + 5
}
}