I am attempting to make an analog clock using CoreGraphics.
My problem is that I do not know how to rotate the text of the numerals.
The general process of the code is as follows:
First I rotate the context, then, if applicable I draw a number; repeat.
The complete Playground
import UIKit
import Foundation
import XCPlayground
class ClockFace: UIView {
func drawText(context: CGContextRef, text: NSString, attributes: [String: AnyObject], x: CGFloat, y: CGFloat) -> CGSize {
CGContextTranslateCTM(context, 0, 0)
CGContextScaleCTM(context, 1, -1)
let font = attributes[NSFontAttributeName] as! UIFont
let attributedString = NSAttributedString(string: text as String, attributes: attributes)
let textSize = text.sizeWithAttributes(attributes)
let textPath = CGPathCreateWithRect(CGRect(x: -x, y: y + font.descender, width: ceil(textSize.width), height: ceil(textSize.height)), nil)
let frameSetter = CTFramesetterCreateWithAttributedString(attributedString)
let frame = CTFramesetterCreateFrame(frameSetter, CFRange(location: 0, length: attributedString.length), textPath, nil)
CTFrameDraw(frame, context)
return textSize
}
override func drawRect(rect: CGRect) {
let ctx: CGContext? = UIGraphicsGetCurrentContext()
var nums: Int = 0
for i in 0..<60 {
CGContextSaveGState(ctx)
CGContextTranslateCTM(ctx, rect.width / 2, rect.height / 2)
CGContextRotateCTM(ctx, CGFloat(6.0 * Double(i) * M_PI / 180))
CGContextMoveToPoint(ctx, 72, 0)
if (i % 5 == 0) {
nums++
drawText(ctx!, text: "\(nums)", attributes: [NSForegroundColorAttributeName : UIColor.blackColor().CGColor, NSFontAttributeName : UIFont.systemFontOfSize(12)], x: 42, y: 0)
}
CGContextRestoreGState(ctx)
}
}
}
let myView = ClockFace(frame: CGRectMake(0, 0, 150, 150))
myView.backgroundColor = .lightGrayColor()
XCPShowView("", view: myView)
Hence my question, how does one rotate Core Text with Swift?
These 15 lines took me over 8 hours, gobs of StackOverflow searches (many with answers that didn't/no long work!), to figure out.
Beware of solutions that use CTLineDraw. For CTLineDraw, you have to change cgContext.textMatrix to do the rotation and I got very strange/unreliable results. Generating a frame and then rotating that frame proved MUCH more reliable.
Using Swift 5.2...
func drawCenteredString(
cgContext: CGContext,
string: String,
targetCenter: CGPoint,
angleClockwise: Angle,
uiFont: UIFont
) {
let attrString = NSAttributedString(string: string, attributes: [NSAttributedString.Key.font: uiFont])
let textSize = attrString.size()
let textPath = CGPath(rect: CGRect(x: CGFloat(-textSize.width/2), y: -uiFont.ascender / 2, width: ceil(textSize.width), height: ceil(textSize.height)), transform: nil)
let frameSetter = CTFramesetterCreateWithAttributedString(attrString)
let frame = CTFramesetterCreateFrame(frameSetter, CFRange(location: 0, length: attrString.length), textPath, nil)
cgContext.saveGState()
cgContext.translateBy(x: targetCenter.x, y: targetCenter.y)
cgContext.scaleBy(x: 1, y: -1)
cgContext.rotate(by: CGFloat(-angleClockwise.radians))
CTFrameDraw(frame, cgContext)
cgContext.restoreGState()
}
You can implement CGAffineTransform via CGContextSetTextMatrix() to rotate texts. Ex:
...
let frame = CTFramesetterCreateFrame(frameSetter, CFRange(location: 0, length: attributedString.length), textPath, nil)
CGContextSetTextMatrix(context, CGAffineTransformMakeRotation(CGFloat(M_PI)))
CTFrameDraw(frame, context)
Related
hi i am a beginner in speed writing
I refer to this writing method and apply it in my segment controller
I am using swift 5.7 method
This can be successfully used and executed with
But i have a problem that i can't change the color of my text and icon
I try to add something in "string.draw" but it dosen't work
The method I use is as follows
class func textEmbededImage(image: UIImage, string: String, color: UIColor, imageAlignment: Int = 0) -> UIImage {
let font = UIFont.systemFont(ofSize: 13.0)
let expectedTextSize: CGSize = (string as NSString).size(withAttributes: [NSAttributedString.Key.font: font])
let width: CGFloat = expectedTextSize.width + image.size.width + 5.0
let height: CGFloat = max(expectedTextSize.height, image.size.width)
let size: CGSize = CGSize(width: width, height: height)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
let context: CGContext = UIGraphicsGetCurrentContext()!
context.setFillColor(color.cgColor)
let fontTopPosition: CGFloat = (height - expectedTextSize.height) / 2.0
let textOrigin: CGFloat = (imageAlignment == 0) ? image.size.width + 5 : 0
let textPoint: CGPoint = CGPoint.init(x: textOrigin, y: fontTopPosition)
string.draw(at: textPoint, withAttributes: [.font : font , .foregroundColor : UIColor.gray])
let flipVertical: CGAffineTransform = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: size.height)
context.concatenate(flipVertical)
let alignment: CGFloat = (imageAlignment == 0) ? 0.0 : expectedTextSize.width + 5.0
context.draw(image.cgImage!, in: CGRect.init(x: alignment, y: ((height - image.size.height) / 2.0), width: image.size.width, height: image.size.height))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext(
return newImage!
}
and i use like this
customSegment.setImage(UIImage.textEmbededImage(image: UIImage.add, string: "QQQ", color: .red),forSegmentAt: 0)
customSegment.setImage(UIImage.textEmbededImage(image: UIImage.add, string: "AAA", color: .white),forSegmentAt: 1)
customSegment.setImage(UIImage.textEmbededImage(image: UIImage.add, string: "BBB", color: .red),forSegmentAt: 2)
but this doesn't work for
I went into debug view hierarchy xcode to try to find the problem
debug view hierarchy
But what puzzles me even more is that what I see on the right layer is the gray color I set, but what I see on the screen is the blue color set by the system
How can I solve it
In addition, I also want to ask, how to make different text colors to achieve the effect when selecting
I am trying to draw a few strings in a custom view with swift. I have done the same thing on Android with Java, and here's the result:
With Swift, I get this graphic:
Here's my code:
let π:CGFloat = CGFloat(M_PI)
extension NSString {
func drawWithBasePoint(basePoint: CGPoint, andAngle angle: CGFloat, andAttributes attributes: [String: AnyObject]) {
let textSize: CGSize = self.sizeWithAttributes(attributes)
let context: CGContextRef = UIGraphicsGetCurrentContext()!
let t: CGAffineTransform = CGAffineTransformMakeTranslation(basePoint.x, basePoint.y)
let r: CGAffineTransform = CGAffineTransformMakeRotation(angle)
CGContextConcatCTM(context, t)
CGContextConcatCTM(context, r)
self.drawAtPoint(CGPointMake(textSize.width / 2, -textSize.height / 2), withAttributes: attributes)
CGContextConcatCTM(context, CGAffineTransformInvert(r))
CGContextConcatCTM(context, CGAffineTransformInvert(t))
}
}
override func drawRect(rect: CGRect) {
elements = delegate!.getRotaryElements(self)
let center = CGPoint(x:bounds.width / 2, y: bounds.height / 2)
let sectorSize = 2 * π / CGFloat.init(elements.count)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = NSTextAlignment.Center
for i in 0...elements.count - 1 {
let attrs = [NSFontAttributeName: UIFont(name: "HelveticaNeue-Thin", size: 14)!, NSParagraphStyleAttributeName: paragraphStyle, NSForegroundColorAttributeName:(i % 2 == 0 ? UIColor.redColor():primaryColor)]
elements[i].drawWithBasePoint(center, andAngle: CGFloat(i) * sectorSize + (sectorSize / 2), andAttributes: attrs)
}
}
How can I produce the same thing in Swift? I am new to Swift (and iOS for that matter) and would appreciate any hint you can give me.
Try the code below, and to see if it works.
let π:CGFloat = CGFloat(M_PI)
extension NSString {
func drawWithBasePoint(basePoint: CGPoint, andAngle angle: CGFloat, andAttributes attributes: [String: AnyObject]) {
let radius: CGFloat = 100
let textSize: CGSize = self.sizeWithAttributes(attributes)
let context: CGContextRef = UIGraphicsGetCurrentContext()!
let t: CGAffineTransform = CGAffineTransformMakeTranslation(basePoint.x, basePoint.y)
let r: CGAffineTransform = CGAffineTransformMakeRotation(angle)
CGContextConcatCTM(context, t)
CGContextConcatCTM(context, r)
self.drawAtPoint(CGPointMake(radius-textSize.width/2, -textSize.height/2), withAttributes: attributes)
CGContextConcatCTM(context, CGAffineTransformInvert(r))
CGContextConcatCTM(context, CGAffineTransformInvert(t))
}
}
In Swift 5.0:
extension String {
func drawWithBasePoint(basePoint: CGPoint,
andAngle angle: CGFloat,
andAttributes attributes: [NSAttributedString.Key : Any]) {
let textSize: CGSize = self.size(withAttributes: attributes)
let context: CGContext = UIGraphicsGetCurrentContext()!
let t: CGAffineTransform = CGAffineTransform(translationX: basePoint.x, y: basePoint.y)
let r: CGAffineTransform = CGAffineTransform(rotationAngle: angle)
context.concatenate(t)
context.concatenate(r)
self.draw(at: CGPoint(x: textSize.width / 2, y: -textSize.height / 2), withAttributes: attributes)
context.concatenate(r.inverted())
context.concatenate(t.inverted())
}
}
I am attempting to rotate Core Text such that it is readable despite it being generated via a rotated context.
My issue is that regardless of the rotation method, the resulting text is not what is expected.
Here is the working playground:
import Foundation
import UIKit
import XCPlayground
class ClockFace: UIView {
func drawText(context: CGContextRef, text: NSString, attributes: [String: AnyObject], x: CGFloat, y: CGFloat, align: CGFloat) -> CGSize {
CGContextTranslateCTM(context, 0, 0)
CGContextScaleCTM(context, 1, -1)
let font = attributes[NSFontAttributeName] as! UIFont
let attributedString = NSAttributedString(string: text as String, attributes: attributes)
let textSize = text.sizeWithAttributes(attributes)
let textPath = CGPathCreateWithRect(CGRect(x: -x, y: y + font.descender, width: ceil(textSize.width), height: ceil(textSize.height)), nil)
let frameSetter = CTFramesetterCreateWithAttributedString(attributedString)
let frame = CTFramesetterCreateFrame(frameSetter, CFRange(location: 0, length: attributedString.length), textPath, nil)
CGContextSetTextMatrix(context, CGAffineTransformMakeRotation(align * 43.0))
CTFrameDraw(frame, context)
return textSize
}
override func drawRect(rect: CGRect) {
let ctx: CGContext? = UIGraphicsGetCurrentContext()
var nums: Int = 0
for i in 0..<60 {
CGContextSaveGState(ctx)
CGContextTranslateCTM(ctx, rect.width / 2, rect.height / 2)
CGContextRotateCTM(ctx, CGFloat(6.0 * Double(i) * M_PI / 180))
if (i % 5 == 0) {
nums++
drawText(ctx!, text: "\(nums)", attributes: [NSForegroundColorAttributeName : UIColor.blackColor().CGColor, NSFontAttributeName : UIFont.systemFontOfSize(12)], x: 42, y: 0, align: CGFloat(6.0 * Double(i) * M_PI / 180))
}
CGContextRestoreGState(ctx)
}
}
}
let myView = ClockFace(frame: CGRectMake(0, 0, 150, 150))
myView.backgroundColor = .lightGrayColor()
XCPShowView("", view: myView)
Regardless of the value in CGAffineTransfomMakeRotation the text is illegible.
Why is this? Am I rotating the text correctly?
Regards, Brandon
I draw a clock face based on your codes but revised little bit.
Basically what I did is to flip and move whole clock face outside the for-loop. You can see I moved CGContextTranslateCTM() and CGContextScaleCTM() from drawText() to drawRect(). Others are some minor calculation changes owing to above modification.
As to CGContextSetTextMatrix(), the rotation happens here is affecting each character but not whole string. You can comment line 19 & 20 and uncomment line 21 to have a different result and see what I mean.
Also if you would like to get the playground file directly, you can download it from here.
import Foundation
import UIKit
import XCPlayground
class ClockFace: UIView {
func drawText(context: CGContextRef, text: String, attributes: [String: AnyObject], x: CGFloat, y: CGFloat, align: CGFloat) -> CGSize {
let font = attributes[NSFontAttributeName] as! UIFont
let attributedString = NSAttributedString(string: text, attributes: attributes)
let textSize = text.sizeWithAttributes(attributes)
let textPath = CGPathCreateWithRect(CGRect(x: x, y: y + font.descender, width: ceil(textSize.width), height: ceil(textSize.height)), nil)
let frameSetter = CTFramesetterCreateWithAttributedString(attributedString)
let frame = CTFramesetterCreateFrame(frameSetter, CFRange(location: 0, length: attributedString.length), textPath, nil)
var transform = CGAffineTransformMakeTranslation(x + 10, y + font.descender)
transform = CGAffineTransformRotate(transform, align)
//CGContextSetTextMatrix(context, CGAffineTransformMakeRotation(align))
CGContextConcatCTM(context, transform);
CTFrameDraw(frame, context)
return textSize
}
override func drawRect(rect: CGRect) {
let ctx: CGContext? = UIGraphicsGetCurrentContext()
CGContextTranslateCTM(ctx, -45, rect.size.height + 5)
CGContextScaleCTM(ctx, 1, -1)
var nums: Int = 2
for i in 0..<60 {
CGContextSaveGState(ctx)
CGContextTranslateCTM(ctx, rect.width / 2, rect.height / 2)
CGContextRotateCTM(ctx, CGFloat(-6.0 * Double(i) * M_PI / 180))
if (i % 5 == 0) {
nums++
nums = nums > 12 ? nums - 12 : nums
drawText(ctx!, text: "\(nums)", attributes: [NSForegroundColorAttributeName : UIColor.whiteColor(), NSFontAttributeName : UIFont.systemFontOfSize(12)], x: 42, y: 0, align: CGFloat(6.0 * Double(i) * M_PI / 180))
}
CGContextRestoreGState(ctx)
}
}
}
let view = ClockFace(frame: CGRectMake(0, 0, 150, 150))
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
XCPlaygroundPage.currentPage.liveView = view
Currently I am using UILabel to put texts on the screen:
var label = UILabel()
view.addSubView(label)
label.backgroundColor = UIColor.whiteColor()
label.numberOfLines = 2
label.font = UIFont.systemFontOfSize(20, weight: UIFontWeightMedium)
label.frame = CGRectMake(0, 0, 100, 50)
label.text = "Hi, this is some text..."
How to use CoreText to replace the above in Swift?
Following code solved my question:
override func drawRect(rect: CGRect) {
super.drawRect(rect)
let context = UIGraphicsGetCurrentContext()
// Flip the coordinate system
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
let path = CGPathCreateMutable()
CGPathAddRect(path, nil, self.bounds)
let str = NSMutableAttributedString(string: "some text goes here...")
// set font color
str.addAttribute(kCTForegroundColorAttributeName as String, value:UIColor.blackColor() , range: NSMakeRange(0,str.length))
// set font name & size
let fontRef = UIFont.systemFontOfSize(20, weight: UIFontWeightMedium)
str.addAttribute(kCTFontAttributeName as String, value: fontRef, range:NSMakeRange(0, str.length))
let frameSetter = CTFramesetterCreateWithAttributedString(str)
let ctFrame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0,str.length), path, nil)
CTFrameDraw(ctFrame, context!)
}
Update for Swift 4.0:
override func draw(_ rect: CGRect) {
super.draw(rect)
// context allows you to manipulate the drawing context (i'm setup to draw or bail out)
guard let context: CGContext = UIGraphicsGetCurrentContext() else {
return
}
// Flip the coordinate system
context.textMatrix = CGAffineTransform.identity;
context.translateBy(x: 0, y: self.bounds.size.height);
context.scaleBy(x: 1.0, y: -1.0);
let path = CGMutablePath()
path.addRect(self.bounds)
let str = NSMutableAttributedString(string: "00:00:00")
// set font color
str.addAttribute(NSAttributedStringKey(rawValue: kCTForegroundColorAttributeName as String), value:UIColor.purple , range: NSMakeRange(0,str.length))
// set font name & size
let fontRef = UIFont.systemFont(ofSize: 20, weight: UIFont.Weight.bold)
str.addAttribute(NSAttributedStringKey(rawValue: kCTFontAttributeName as String), value: fontRef, range:NSMakeRange(0, str.length))
let frameSetter = CTFramesetterCreateWithAttributedString(str)
let ctFrame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0,str.length), path, nil)
CTFrameDraw(ctFrame, context)
}
Try this:
func drawText(context: CGContextRef, text: NSString, attributes: [String: AnyObject], x: CGFloat, y: CGFloat) -> CGSize {
let font = attributes[NSFontAttributeName] as UIFont
let attributedString = NSAttributedString(string: text, attributes: attributes)
let textSize = text.sizeWithAttributes(attributes)
// y: Add font.descender (its a negative value) to align the text at the baseline
let textPath = CGPathCreateWithRect(CGRect(x: x, y: y + font.descender, width: ceil(textSize.width), height: ceil(textSize.height)), nil)
let frameSetter = CTFramesetterCreateWithAttributedString(attributedString)
let frame = CTFramesetterCreateFrame(frameSetter, CFRange(location: 0, length: attributedString.length), textPath, nil)
CTFrameDraw(frame, context)
return textSize
}
And call like this:
var textAttributes: [String: AnyObject] = [
NSForegroundColorAttributeName : UIColor(white: 1.0, alpha: 1.0).CGColor,
NSFontAttributeName : UIFont.systemFontOfSize(17)
]
drawText(ctx, text: "Hello, World!", attributes: textAttributes, x: 50, y: 50)
Hope this works!
Updated for swift 3.0:
if let context = UIGraphicsGetCurrentContext(){
// Flip the coordinate system
context.textMatrix = CGAffineTransform.identity;
context.translateBy(x: 0, y: self.bounds.size.height);
context.scaleBy(x: 1.0, y: -1.0);
let path = CGMutablePath()
path.addRect(self.bounds)
let str = NSMutableAttributedString(string: "some text goes here...")
// set font color
str.addAttribute(kCTForegroundColorAttributeName as String, value:UIColor.black , range: NSMakeRange(0,str.length))
// set font name & size
let fontRef = UIFont.systemFont(ofSize: 20, weight: UIFontWeightMedium)
str.addAttribute(kCTFontAttributeName as String, value: fontRef, range:NSMakeRange(0, str.length))
let frameSetter = CTFramesetterCreateWithAttributedString(str)
let ctFrame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0,str.length), path, nil)
CTFrameDraw(ctFrame, context)
}
So I have the following code that rotates the CGContext to position where my text should be:
var nums: Int = 0
let oNums: [Int] = [3,4,5,6,7,8,9,10,11,12,1,2]
for i in 0..<60 {
CGContextSaveGState(ctx)
CGContextTranslateCTM(ctx, rect.width / 2, rect.height / 2)
CGContextRotateCTM(ctx, CGFloat(6.0 * Double(i) * M_PI / 180))
CGContextSetStrokeColorWithColor(ctx, UIColor.grayColor().CGColor)
CGContextMoveToPoint(ctx, 72, 0)
CGContextSetLineWidth(ctx, 3.0)
CGContextAddLineToPoint(ctx, 42, 0)
drawText(ctx!, text: getRoman(oNums[nums]), attributes: [NSForegroundColorAttributeName : UIColor.blackColor().CGColor, NSFontAttributeName : UIFont.systemFontOfSize(12)], x: 42, y: 0, align: CGFloat(6.0 * Double(i) * M_PI / 180))
nums++
}
Where drawText(_:) is:
func drawText(context: CGContextRef, text: NSString, attributes: [String: AnyObject], x: CGFloat, y: CGFloat, align: CGFloat) -> CGSize {
CGContextTranslateCTM(context, 0, 0)
CGContextScaleCTM(context, 1, -1)
let font = attributes[NSFontAttributeName] as! UIFont
let attributedString = NSAttributedString(string: text as String, attributes: attributes)
let textSize = text.sizeWithAttributes(attributes)
let textPath = CGPathCreateWithRect(CGRect(x: x, y: y + font.descender, width: ceil(textSize.width), height: ceil(textSize.height)), nil)
let frameSetter = CTFramesetterCreateWithAttributedString(attributedString)
let frame = CTFramesetterCreateFrame(frameSetter, CFRange(location: 0, length: attributedString.length), textPath, nil)
CTFrameDraw(frame, context)
return textSize
}
And getRoman(_:) is:
func getRoman(numeral: Int) -> String {switch numeral {case 1:return "I"case 2:return "II"case 3:return "III"case 4:return "IV"case 5:return "V"case 6:return "VI"case 7:return "VII"case 8:return "VIII"case 9:return "IX"case 10:return "X"case 11:return "XI"case 12:return "XII"default:return ""}}
The issue, is that this prints text rotated to an angle (the desired result would be text all angled to the same degree (standard text orientation))
Is there a way to rotate Core Text?
Regards, Brandon