Drawing NSAttributedString with Core Text centered - ios

I have a method that draws an NSAttributedString in a rect:
-(void)drawInRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)self);
// left column form
CGMutablePathRef leftColumnPath = CGPathCreateMutable();
CGPathAddRect(leftColumnPath, NULL, CGRectMake(rect.origin.x, -rect.origin.y, rect.size.width, rect.size.height));
// left column frame
CGFloat translateAmount = rect.size.height;
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, translateAmount);
CGContextScaleCTM(context, 1.0, -1.0);
CTFrameRef leftFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), leftColumnPath, NULL);
CTFrameDraw(leftFrame, context);
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, translateAmount);
CGContextScaleCTM(context, 1.0, -1.0);
CFRelease(leftFrame);
CFRelease(framesetter);
CGPathRelease(leftColumnPath);
}
I put this together a couple months ago with some help from tutorials. It turns out though that this, by default, draws the string left aligned. I'm not too crafty with Core Text, does anyone know how I might be able to draw this with text alignment center?
(Please don't recommend outside label drawing class, I need to do this with Core Text).

Set the CTTextAlignment of the CTParagraphStyle to kCTCenterTextAlignment for the range of the attributed string you want centered.
Then create the CTFramesetter, then draw.
Example: http://foobarpig.com/iphone/coretext-set-text-font-and-alignment-to-cfattributedstring.html

for swift 3.0:
final private func drawIn(rect: CGRect, attribString: NSAttributedString ){
guard let context = UIGraphicsGetCurrentContext() else{
return
}
let framesetter = CTFramesetterCreateWithAttributedString(attribString)
// left column form
let leftColumnPath = CGMutablePath()
leftColumnPath.addRect(CGRect(x:rect.origin.x,
y: -rect.origin.y,
width: rect.size.width,
height: rect.size.height)
)
// left column frame
let translateAmount = rect.size.height
context.saveGState()
context.textMatrix = CGAffineTransform.identity
context.translateBy(x: 0, y: translateAmount)
context.scaleBy(x: 1.0, y: -1.0)
let leftFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), leftColumnPath, nil)
CTFrameDraw(leftFrame, context)
context.textMatrix = CGAffineTransform.identity
context.translateBy(x: 0, y: translateAmount)
context.scaleBy(x: 1.0, y: -1.0)
}
final private func drawStopMessage(){
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let attributes = [NSParagraphStyleAttributeName : paragraphStyle,
NSFontAttributeName : UIFont.systemFont(ofSize: 24.0),
NSForegroundColorAttributeName : UIColor.blue,
]
let attrString = NSAttributedString(string: "Stop\nall Dance",
attributes: attributes)
let rect = CGRect(x: 20, y: 100, width: 300, height: 300)
drawIn(rect: rect, attribString: attrString)
}

Related

How can I convert the following code about CGContext into Swift3

Just like the headline ,I have some Objective-c code ,how can I use them in Swift3
CGContextSaveGState(context);
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
NSMutableAttributedString *attri = [[NSMutableAttributedString alloc]initWithString:_text];
[attri addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:10] range:NSMakeRange(0, _text.length)];
CTFramesetterRef ctFramesetting = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attri);
CTFrameRef ctFrame = CTFramesetterCreateFrame(ctFramesetting, CFRangeMake(0, attri.length), path, NULL);
CTFrameDraw(ctFrame, context);
CFRelease(path);
CFRelease(ctFramesetting);
CFRelease(ctFrame);
Here's a clean Swift 3 version:
context.saveGState()
context.textMatrix = CGAffineTransform.identity
context.translateBy(x: 0, y: size.height)
context.scaleBy(x: 1.0, y: -1.0)
let path = CGMutablePath()
let rect = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)
path.addRect(rect, transform: .identity)
let attrString = NSMutableAttributedString(string: _text as String)
attrString.addAttribute(NSFontAttributeName,
value: UIFont.systemFont(ofSize: 10.0),
range: NSRange(location: 0,
length: _text.length))
let ctFramesetting = CTFramesetterCreateWithAttributedString(attrString)
let ctFrame = CTFramesetterCreateFrame(ctFramesetting,
CFRangeMake(0, attrString.length),
path,
nil)
CTFrameDraw(ctFrame, context)
I advise you not to use converters.
Why?
With converters you'll
probably use variables where you need constants
bridge/cast values
explicitly unwrap optionals
break Swift style (this depends on what style you use actually, but still)
break language conventions
this means you'll get unstable/dirty code that you need to refactor
You can use something like the Objective-C to Swift Converter to get code like the following:
context.saveGState()
context.textMatrix = CGAffineTransform.identity
context.translateBy(x: 0, y: size.height)
context.scaleBy(x: 1.0, y: -1.0)
var path: CGMutablePathRef = CGMutablePath()
path.addRect(CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(size.width), height: CGFloat(size.height)), transform: .identity)
var attri = NSMutableAttributedString(string: _text)
attri.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: CGFloat(10)), range: NSRange(location: 0, length: _text.length))
var ctFramesetting: CTFramesetterRef? = CTFramesetterCreateWithAttributedString((attri as? CFAttributedStringRef))
var ctFrame: CTFrameRef = CTFramesetterCreateFrame(ctFramesetting, CFRangeMake(0, attri.length), path, nil)
CTFrameDraw(ctFrame, context)
But I would suggest that you do learn the underlying principles of both Objective-C and Swift so that you can do this sort of conversion yourself instead of relying on others or an external tool :)
For example, the above is the code as it came out from the converter. However, if you were to do some clean up, it would look something like this:
guard let context = UIGraphicsGetCurrentContext() else {
return
}
context.saveGState()
context.textMatrix = CGAffineTransform.identity
context.translateBy(x: 0, y: size.height)
context.scaleBy(x:1.0, y: -1.0)
let path = CGMutablePath()
path.addRect(CGRect(x:0, y:0, width:size.width, height:size.height), transform:.identity)
let attri = NSMutableAttributedString(string:_text as String)
attri.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize:10), range: NSRange(location: 0, length:_text.length))
let ctFramesetting = CTFramesetterCreateWithAttributedString(attri)
let ctFrame = CTFramesetterCreateFrame(ctFramesetting, CFRangeMake(0, attri.length), path, nil)
CTFrameDraw(ctFrame, context)
As you'll notice the following has to be added to get a CGContext instance:
guard let context = UIGraphicsGetCurrentContext() else {
return
}
Also, some variables which were marked as mutable (or var) have been changed to immutable (or let) instances.
But that's not all there is to it. The above code assumes that _text is of type NSString. In order to use native Swift types, you should really change it to use String instead of NSString. Then the code changes to:
guard let context = UIGraphicsGetCurrentContext() else {
return
}
context.saveGState()
context.textMatrix = CGAffineTransform.identity
context.translateBy(x: 0, y: size.height)
context.scaleBy(x:1.0, y: -1.0)
let path = CGMutablePath()
path.addRect(CGRect(x:0, y:0, width:size.width, height:size.height), transform:.identity)
let attri = NSMutableAttributedString(string:_text)
attri.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize:10), range: NSRange(location: 0, length:_text.characters.count))
let ctFramesetting = CTFramesetterCreateWithAttributedString(attri)
let ctFrame = CTFramesetterCreateFrame(ctFramesetting, CFRangeMake(0, attri.length), path, nil)
CTFrameDraw(ctFrame, context)
So, in order to convert the code to Swift 3 and to make it work, you do need to understand the differences between Swift and Objective-C.

How to Rotate Core Text with Swift?

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)

How to use CoreText to replace UILabel in Swift?

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

CGContextSetShadowWithColor puts shadows on top of the text casting the shadow

I am using CGContextSetShadowWithColor and core graphics to draw shadowed text. The shadow appears, but it also seems to "muddy" the actual text itself which is casting the shadow (which should be pure white). It is as if it is casting the shadow on top of the text (but not quite).
Like this:
If I redraw the text in the same position with shadowing off, I can overwrite the muddy text with clean white text, so it is a work-around, but I am wondering:
Am I am doing something wrong, or is this a bug?
let shadowOffset : CGSize = CGSize (width: 4, height: 4)
UIGraphicsBeginImageContextWithOptions(CGSize(width: 800, height: 200), false, 1.0)
let ctx = UIGraphicsGetCurrentContext()
CGContextTranslateCTM(ctx, 0, 200);
CGContextScaleCTM(ctx, 1.0, -1.0);
CGContextSetAlpha(ctx, 1.0)
CGContextSetShadowWithColor(ctx, shadowOffset, 5, UIColor(red: 0, green: 0, blue: 0, alpha: 0.5).CGColor)
CGContextSetAllowsAntialiasing (ctx, true)
let attr:CFDictionaryRef = [
NSFontAttributeName : UIFont(name: fontName, size: fontSize)!,
NSForegroundColorAttributeName:UIColor.whiteColor()]
let line = CTLineCreateWithAttributedString(CFAttributedStringCreate(nil, "1234567890", attr))
let bounds = CTLineGetBoundsWithOptions(line, CTLineBoundsOptions.UseOpticalBounds)
CGContextSetLineWidth(ctx, 1)
CGContextSetTextDrawingMode(ctx, kCGTextFillStroke)
CGContextSetTextPosition(ctx, 100.0, 100.0)
CTLineDraw(line, ctx)
//Uncomment to clean-up text
//CGContextSetShadowWithColor(ctx, shadowOffset, 0, nil)
//CGContextSetTextPosition(ctx, 100.0, 100.0)
//CTLineDraw(line, ctx)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
When calling to CGContextSetTextDrawingMode, set the drawing mode to kCGTextFill. To my understanding, the shadow you see is casted by the stroke of the text.

Why the image is rotated , by calling CGContextDrawImage

Why the image is rotated , by calling CGContextDrawImage.Thanks for your help.
// Initialization code
UIImage *img = [UIImage imageNamed:#"logo.png"];
UIImagePNGRepresentation(img);
_image_ref = img.CGImage;
// Drawing code
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect img_rect = CGRectMake(20, 40, 100, 150);
CGContextDrawImage(context, img_rect, _image_ref);
core graphics's coordinated system not like UIKit, you need to calculate the right coordinate.
http://blog.ddg.com/?p=10
Following this explanation. I created solution which allow to draw multiple images with custom rects in one context.
func foo() -> UIImage? {
let image = UIImage(named: "back.png")!
let contextSize = CGSize(width: 500, height: 500)
UIGraphicsBeginImageContextWithOptions(contextSize, true, image.scale)
guard let ctx = UIGraphicsGetCurrentContext() else { return nil }
guard let cgImage = image.cgImage else { return nil}
//Start code which can by copy/paste
let imageRect = CGRect(origin: CGPoint(x: 200.0, y: 200.0), size: image.size) //custom rect
let ty = imageRect.origin.y + imageRect.size.height //calculate translation Y
let imageRectWithoutOriginY = CGRect(origin: CGPoint(x: imageRect.origin.x, y: 0), size: imageRect.size)
ctx.translateBy(x: 0.0, y: ty) //prepare context for custom rect
ctx.scaleBy(x: 1.0, y: -1.0)
ctx.draw(cgImage, in: imageRectWithoutOriginY) //draw image
ctx.translateBy(x: 0.0, y:-ty) //restore default context setup (so you can select new area to place another image)
ctx.scaleBy(x: 1.0, y: -1.0)
//End code which can by copy/paste
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return result
}
Example with to images:
I know that it can be refactored. I duplicated code for more clarity.

Resources