Remove UILabel top and bottom free space - swift

Hi all. I'm trying to find a way to remove the white space (circled in the screenshot) above and below the text in a UILabel. I googled a lot, but i can't figure out. Thanks for help!
As #Sweeper asks, i attaching my code
#IBDesignable final class LabelWithoutPadding: UILabel {
override func drawText(in rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {
// At the first, i thought that it was insets, but this did not help at all
// So this code doesn't fix my problem
let insets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
super.drawText(in: rect.inset(by: insets))
return
}
// Source https://stackoverflow.com/questions/35335068/how-to-use-coretext-to-replace-uilabel-in-swift
context.textMatrix = CGAffineTransform.identity;
// You can see height - 4
/*
It helped, but only for the bottom.
And it seems to me that this is a completely wrong decision.
Also, some labels stopped showing text because of this and the code below,
so I think that the solution has not been found.
*/
context.translateBy(x: 0, y: self.bounds.size.height - 4);
context.scaleBy(x: 1.0, y: -1.0);
let path = CGMutablePath()
path.addRect(self.bounds)
let str = NSMutableAttributedString(string: self.text ?? "")
// set font color
str.addAttribute(NSAttributedString.Key(rawValue: kCTForegroundColorAttributeName as String), value: self.textColor ?? Colors.contrastHighter , range: NSMakeRange(0, str.length))
// set font name & size
let fontRef = self.font ?? FontMaker.getFont(fontSize: 12, fontWeight: 400)
str.addAttribute(NSAttributedString.Key(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)
}
}

Related

Drawing Text to a CGContext for Quartz PDF not working

This is for Swift 5 on macOS
I am trying to write some text to a generated PDF.
I am able to load a background image onto the pages, but when I call my drawText method, it is not making it onto either of the pages.
I tried drawing an NSString to the context via the .draw() method and that would not work either. I hoping to get this to work so I can add more text, including text boxes, etc.
What am I doing wrong? Thanks for any pointers.
import Cocoa
import CoreText
import Quartz
extension NSImage {
/*
Converts an NSImage to a CGImage for rendering in a CGContext
Credit - Xue Yu
- https://gist.github.com/KrisYu/83d7d97cae35a0b10fd238e5c86d288f
*/
var toCGImage: CGImage {
var imageRect = NSRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
guard let image = cgImage(forProposedRect: &imageRect, context: nil, hints: nil) else {
abort()
}
return image
}
}
class PDFText {
/*
Create a non-nil CGContext
Credit - hmali - 3/15/2019
https://stackoverflow.com/questions/41100895/empty-cgcontext
*/
var pdfContext = CGContext(data: nil,
width: 0,
height: 0,
bitsPerComponent: 1,
bytesPerRow: 1,
space: CGColorSpace.init(name: CGColorSpace.sRGB)!,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
let textRect = CGRect(x: 295, y: 350, width: 100, height: 100)
func createPDF() {
let filePath = "/Users/Shared/Text.pdf"
let fileURL = NSURL(fileURLWithPath: filePath)
pdfContext = CGContext(fileURL, mediaBox: &backgroundRect, nil)
pdfContext!.beginPDFPage(nil)
drawBackground()
drawText("This is page 1")
pdfContext!.endPDFPage()
pdfContext!.beginPDFPage(nil)
drawBackground()
drawText("This is page 1")
pdfContext!.endPDFPage()
pdfContext!.closePDF()
}
func drawBackground() {
let cgImage = NSImage(contentsOfFile: "/Users/Shared/background.png")?.toCGImage
pdfContext?.draw(cgImage!, in: CGRect(x: 0, y: 0, width: Int(72*8.5), height: Int(72*11)))
}
func drawText(_ text:String) {
let style = NSMutableParagraphStyle()
style.alignment = .center
let attr = [NSAttributedString.Key.font: NSFont(name: "Helvetica", size: 16.0),
NSAttributedString.Key.foregroundColor: NSColor.purple,
NSAttributedString.Key.backgroundColor: NSColor.clear,
NSAttributedString.Key.paragraphStyle: style]
let attrText = NSAttributedString(string: text, attributes: attr as [NSAttributedString.Key : Any])
pdfContext?.saveGState()
pdfContext?.translateBy(x: attrText.size().width, y: attrText.size().height)
attrText.draw(with: textRect)
pdfContext?.restoreGState()
}
}
Closing an open question that I got worked out (complete code).
Swift 5.4 on macOS
import Cocoa
import CoreText
import Quartz
var pageWidth: CGFloat = 72*8.5
var pageHeight: CGFloat = 72*11.0
var pageRect: CGRect = CGRect(x:0, y:0, width: pageWidth, height: pageHeight)
extension NSImage {
/*
Converts an NSImage to a CGImage for rendering in a CGContext
Credit - Xue Yu
- https://gist.github.com/KrisYu/83d7d97cae35a0b10fd238e5c86d288f
*/
var toCGImage: CGImage {
var imageRect = NSRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
guard let image = cgImage(forProposedRect: &imageRect, context: nil, hints: nil) else {
abort()
}
return image
}
}
class PDFText {
/*
Create a non-nil empty CGContext
Credit - hmali - 3/15/2019
https://stackoverflow.com/questions/41100895/empty-cgcontext
*/
var pdfContext = CGContext(data: nil,
width: 0,
height: 0,
bitsPerComponent: 1,
bytesPerRow: 1,
space: CGColorSpace.init(name: CGColorSpace.sRGB)!,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
// Set a rectangle to be in the center of the page
let textRect = CGRect(x: pageRect.midX-50, y: pageRect.midY-50, width: 100, height: 100)
func createPDF() {
let filePath = "/Users/Shared/Text.pdf"
let fileURL = NSURL(fileURLWithPath: filePath)
pdfContext = CGContext(fileURL, mediaBox: &pageRect, nil)
// This must be called to begin a page in a PDF document
pdfContext!.beginPDFPage(nil)
drawBackground()
drawText(text: "This is page 1")
// This has to be called prior to writing another page to the PDF document
pdfContext!.endPDFPage()
pdfContext!.beginPDFPage(nil)
drawBackground()
drawText(text: "This is page 2")
// Call this or before closing the document.
pdfContext!.endPDFPage()
pdfContext!.closePDF()
}
func drawBackground() {
// Draws an image into the graphics context.
// NOTE: If the image is not sized for the specified rectangle it will be
// scaled (up/down) automatically to fit within the rectangle.
let cgImage = NSImage(contentsOfFile: "/Users/Shared/background.png")?.toCGImage
pdfContext?.draw(cgImage!, in: pageRect)
}
func drawText(text:String) {
// Credit: Nutchaphon Rewik, https://github.com/nRewik/SimplePDF
// Create a paragraph style to be used with the atributed string
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
// Set up the sttributes to be applied to the attributed text
let stringAttributes = [NSAttributedString.Key.font: NSFont(name: "Helvetica", size: 16.0),
NSAttributedString.Key.foregroundColor: NSColor.purple,
NSAttributedString.Key.backgroundColor: NSColor.clear,
NSAttributedString.Key.paragraphStyle: paragraphStyle]
// Create the attributed string
let attributedString = NSAttributedString(string: text, attributes: stringAttributes as [NSAttributedString.Key : Any])
// Set up a CoreText frame that encloses the attributed string
let frameSetter = CTFramesetterCreateWithAttributedString(attributedString)
// Get the frame size for the attributed string
let frameSize = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, CFRangeMake(0, attributedString.string.count), nil, textRect.size, nil)
// Save the Graphics state of the context
pdfContext!.saveGState()
// Put the text matrix into a known state. This ensures that no old scaling
// factors are left in place.
pdfContext!.textMatrix = CGAffineTransform.identity
// Create a path object to enclose the text.
let framePath = CGPath(rect: CGRect(x: textRect.minX, y: textRect.midY-frameSize.height/2, width: textRect.width, height: frameSize.height), transform: nil)
// Get the frame that will do the rendering. The currentRange variable specifies
// only the starting point. The framesetter lays out as much text as will fit into
// the frame or until it runs out of text.
let frameRef = CTFramesetterCreateFrame(frameSetter, CFRange(location: 0, length: 0), framePath, nil)
// Draw the CoreText frame (that includes the text) into the graphics context.
CTFrameDraw(frameRef, pdfContext!)
// Restore the previous Graphics state.
pdfContext?.restoreGState()
}
}
let pdf = PDFText()
pdf.createPDF()

How can I show the rest of a character that has gone slightly outside of UITextView?

I have a UITextView which changes size depending on the text the user inputs (the purple box), which is inside another UIView (the red box).
But when using a handwritten style font like this, the end character sometimes gets cut off at the edge:
I have tried used text1.clipsToBounds = false but that didn't show the edge of the character. Is there a way to show the full character without changing the width of the text view?
Also here is the code I am using to set up the text view:
let text1 = UITextView()
text1.text = ""
text1.font = UIFont(name: "Gotcha", size: 27)
text1.frame = CGRect(x: 10, y: 5, width: 70, height: 50)
text1.isScrollEnabled = false
text1.delegate = self
text1.textAlignment = .center
text1.isEditable = false
text1.isSelectable = false
holdingView.addSubview(text1)
The frame then gets updated with this function, and whenever the text is changed:
func adjustTextViewSize(_ textView: UITextView) {
let maxWidth = 300
let newSize = textView.sizeThatFits(CGSize(width: maxWidth, height: CGFloat.greatestFiniteMagnitude))
textView.frame = CGRect(x: (textView.frame.minX), y: (textView.frame.minY), width: newSize.width, height: newSize.height)
}
Thanks!
Update:
I solved this by adding an extra 30px to newSize.width for any font that is handwritten:
if fontFile?.isHandwritten == true {
currentView.widthConstraint?.constant = newSize.width + 30
currentTextHoldingView.widthConstraint?.constant = newSize.width + 30
}
call this function for get height according to string length
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin,
attributes: [NSAttributedString.Key.font: font], context: nil)
return ceil(boundingBox.height)
}
}

Swift: Center NSAttributedString in View horizontally and vertically

Hi there,
I have a class CardButton: UIButton . In the draw method of this CardButton, I would like to add and center(vertically and horizontally) NSAttributed String, which is basically just one Emoji, inside of it. The result would look something like this:
However, NSAttributedString can be only aligned to center in horizontal dimension inside the container.
My idea for solution:
create a containerView inside of CardButton
center containerView both vertically and horizontally in it's container(which is CardButton)
add NSAttributedString inside the containerView and size containerView to fit the string's font.
So the result would look something like this:
My attempt for this to happen looks like this:
class CardButton: UIButton {
override func draw(){
//succesfully drawing the CardButton
let stringToDraw = attributedString(symbol, fontSize: symbolFontSize) //custom method to create attributed string
let containerView = UIView()
containerView.backgroundColor = //green
addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
let centerXConstraint = containerView.centerXAnchor.constraint(equalTo: (self.centerXAnchor)).isActive = true
let centerYConstraint = containerView.centerYAnchor.constraint(equalTo: (self.centerYAnchor)).isActive = true
stringToDraw.draw(in: containerView.bounds)
containerView.sizeToFit()
}
}
Long story short, I failed terribly. I first tried to add containerView to cardButton, made the background green, gave it fixed width and height jut to make sure that it got properly added as a subview. It did. But once I try to active constraints on it, it totally disappears.
Any idea how to approach this?
There is always more than one way to achieve any particular UI design goal, but the procedure below is relatively simple and has been adapted to suit the requirements as presented and understood in your question.
The UIButton.setImage method allows an image to be assigned to a UIButton without the need for creating a container explicitly.
The UIGraphicsImageRenderer method allows an image to be made from various components including NSAttributedText, and a host of custom shapes.
The process utilising these two tools to provide the rudiments for your project will be to:
Render an image with the appropriate components & size
Assign the rendered image to the button
A class could be created for this functionality, but that has not been explored here.
Additionally, your question mentions that when applying constraints the content disappears. This effect can be observed when image dimensions are too large for the container, constraints are positioning the content out of view and possibly a raft of other conditions.
The following code produces the above image:
func drawRectangleWithEmoji() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 512, height: 512))
let img = renderer.image { (ctx) in
// Create the outer square:
var rectangle = CGRect(x: 0, y: 0, width: 512, height: 512).insetBy(dx: 7.5, dy: 7.5)
var roundedRect = UIBezierPath(roundedRect: rectangle, cornerRadius: 50).cgPath
// MARK: .cgPath creates a CG representation of the path
ctx.cgContext.setFillColor(UIColor.white.cgColor)
ctx.cgContext.setStrokeColor(UIColor.blue.cgColor)
ctx.cgContext.setLineWidth(15)
ctx.cgContext.addPath(roundedRect)
ctx.cgContext.drawPath(using: .fillStroke)
// Create the inner square:
rectangle = CGRect(x: 0, y: 0, width: 512, height: 512).insetBy(dx: 180, dy: 180)
roundedRect = UIBezierPath(roundedRect: rectangle, cornerRadius: 10).cgPath
ctx.cgContext.setFillColor(UIColor.green.cgColor)
ctx.cgContext.setStrokeColor(UIColor.green.cgColor)
ctx.cgContext.setLineWidth(15)
ctx.cgContext.addPath(roundedRect)
ctx.cgContext.drawPath(using: .fillStroke)
// Add emoji:
var fontSize: CGFloat = 144
var attrs: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: fontSize)//,
//.backgroundColor: UIColor.gray //uncomment to see emoji background bounds
]
var string = "❤️"
var attributedString = NSAttributedString(string: string, attributes: attrs)
let strWidth = attributedString.size().width
let strHeight = attributedString.size().height
attributedString.draw(at: CGPoint(x: 256 - strWidth / 2, y: 256 - strHeight / 2))
// Add NSAttributedString:
fontSize = 56
attrs = [
.font: UIFont.systemFont(ofSize: fontSize),
.foregroundColor: UIColor.brown
]
string = "NSAttributedString"
attributedString = NSAttributedString(string: string, attributes: attrs)
let textWidth = attributedString.size().width
let textHeight = attributedString.size().height
attributedString.draw(at: CGPoint(x: 256 - textWidth / 2, y: 384 - textHeight / 2))
}
return img
}
Activate the NSLayoutContraints and then the new image can be set for the button:
override func viewDidLoad() {
super.viewDidLoad()
view = UIView()
view.backgroundColor = .white
let buttonsView = UIView()
buttonsView.translatesAutoresizingMaskIntoConstraints = false
// buttonsView.backgroundColor = UIColor.gray
view.addSubview(buttonsView)
NSLayoutConstraint.activate([
buttonsView.widthAnchor.constraint(equalToConstant: CGFloat(buttonWidth)),
buttonsView.heightAnchor.constraint(equalToConstant: CGFloat(buttonWidth)),
buttonsView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
buttonsView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
let cardButton = UIButton(type: .custom)
cardButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
cardButton.setImage(drawRectangleWithEmoji(), for: .normal)
let frame = CGRect(x: 0, y: 0, width: buttonWidth, height: buttonWidth)
cardButton.frame = frame
buttonsView.addSubview(cardButton)
}
Your comments will be appreciated as would constructive review of the code provided.

Image in NSTextAttachment too big and blurry

I'm trying to place an icon (in form of an image) next to a text in a UILabel. The icons are imported into the assets in al three sizes and are not blurry at all when I simply place them in a normal UIImageView.
However, within the NSTextAttachment they suddenly become extremely blurry and are too big, as well.
I already tried several things on my own and also tried nearly every snippet I could find online - nothing helps. This is what I'm left over with:
func updateWinnableCoins(coins: Int){
let attachImg = NSTextAttachment()
attachImg.image = resizeImage(image: #imageLiteral(resourceName: "geld"), targetSize: CGSize(width: 17.0, height: 17.0))
attachImg.setImageHeight(height: 17.0)
let imageOffsetY:CGFloat = -3.0;
attachImg.bounds = CGRect(x: 0, y: imageOffsetY, width: attachImg.image!.size.width, height: attachImg.image!.size.height)
let attchStr = NSAttributedString(attachment: attachImg)
let completeText = NSMutableAttributedString(string: "")
let tempText = NSMutableAttributedString(string: "You can win " + String(coins) + " ")
completeText.append(tempText)
completeText.append(attchStr)
self.lblWinnableCoins.textAlignment = .left;
self.lblWinnableCoins.attributedText = completeText;
}
func resizeImage(image: UIImage, targetSize: CGSize) -> (UIImage) {
let newRect = CGRect(x: 0, y: 0, width: targetSize.width, height: targetSize.height).integral
UIGraphicsBeginImageContextWithOptions(targetSize, false, 0)
let context = UIGraphicsGetCurrentContext()
// Set the quality level to use when rescaling
context!.interpolationQuality = CGInterpolationQuality.default
let flipVertical = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: targetSize.height)
context!.concatenate(flipVertical)
// Draw into the context; this scales the image
context?.draw(image.cgImage!, in: CGRect(x: 0.0,y: 0.0, width: newRect.width, height: newRect.height))
let newImageRef = context!.makeImage()! as CGImage
let newImage = UIImage(cgImage: newImageRef)
// Get the resized image from the context and a UIImage
UIGraphicsEndImageContext()
return newImage
}
extension NSTextAttachment {
func setImageHeight(height: CGFloat) {
guard let image = image else { return }
let ratio = image.size.width / image.size.height
bounds = CGRect(x: bounds.origin.x, y: bounds.origin.y, width: ratio * height, height: height)
}
}
And this is how it looks:
The font size of the UILabel is 17, so I set the text attachment to be 17 big, too. When I set it to 9, it fits, but it's still very blurry.
What can I do about that?

vertically align text in a CATextLayer?

I am working on a CATextLayer that I want to use in both Mac and iOS. Can I control the vertical alignment of the text within the layer?
In this particular case, I want to center it vertically -- but information about other vertical alignments would also be of interest.
EDIT: I found this, but I can't make it work.
The correct answer, as you've already found, is here in Objective-C and works for iOS. It works by subclassing the CATextLayer and overriding the drawInContext function.
However, I've made some improvements to the code, as shown below, using David Hoerl's code as a basis. The changes come solely in recalculating the vertical position of the text represented by the yDiff. I've tested it with my own code.
Here is the code for Swift users:
class LCTextLayer : CATextLayer {
// REF: http://lists.apple.com/archives/quartz-dev/2008/Aug/msg00016.html
// CREDIT: David Hoerl - https://github.com/dhoerl
// USAGE: To fix the vertical alignment issue that currently exists within the CATextLayer class. Change made to the yDiff calculation.
override func draw(in context: CGContext) {
let height = self.bounds.size.height
let fontSize = self.fontSize
let yDiff = (height-fontSize)/2 - fontSize/10
context.saveGState()
context.translateBy(x: 0, y: yDiff) // Use -yDiff when in non-flipped coordinates (like macOS's default)
super.draw(in: context)
context.restoreGState()
}
}
It is an late answer, but I have the same question these days, and have solved the problem with following investigation.
Vertical align depends on the text you need to draw, and the font you are using, so there is no one way solution to make it vertical for all cases.
But we can still calculate the vertical mid point for different cases.
According to apple's About Text Handling in iOS, we need to know how the text is drawn.
For example, I am trying to make vertical align for weekdays strings: Sun, Mon, Tue, ....
For this case, the height of the text depends on cap Height, and there is no descent for these characters. So if we need to make these text align to the middle, we can calculate the offset of the top of cap character, e.g. The position of the top of character "S".
According to the the figure below:
The top space for the capital character "S" would be
font.ascender - font.capHeight
And the bottom space for the capital character "S" would be
font.descender + font.leading
So we need to move "S" a little bit off the top by:
y = (font.ascender - font.capHeight + font.descender + font.leading + font.capHeight) / 2
That equals to:
y = (font.ascender + font.descender + font.leading) / 2
Then I can make the text vertical align middle.
Conclusion:
If your text does not include any character exceed the baseline, e.g. "p", "j", "g", and no character over the top of cap height, e.g. "f". The you can use the formula above to make the text align vertical.
y = (font.ascender + font.descender + font.leading) / 2
If your text include character below the baseline, e.g. "p", "j", and no character exceed the top of cap height, e.g. "f". Then the vertical formula would be:
y = (font.ascender + font.descender) / 2
If your text include does not include character drawn below the baseline, e.g. "j", "p", and does include character drawn above the cap height line, e.g. "f". Then y would be:
y = (font.descender + font.leading) / 2
If all characters would be occurred in your text, then y equals to:
y = font.leading / 2
Maybe to late for answer, but you can calculate size of text and then set position of textLayer. Also you need to put textLayer textAligment mode to "center"
CGRect labelRect = [text boundingRectWithSize:view.bounds.size options:NSStringDrawingUsesLineFragmentOrigin attributes:#{ NSFontAttributeName : [UIFont fontWithName:#"HelveticaNeue" size:17.0] } context:nil];
CATextLayer *textLayer = [CATextLayer layer];
[textLayer setString:text];
[textLayer setForegroundColor:[UIColor redColor].CGColor];
[textLayer setFrame:labelRect];
[textLayer setFont:CFBridgingRetain([UIFont fontWithName:#"HelveticaNeue" size:17.0].fontName)];
[textLayer setAlignmentMode:kCAAlignmentCenter];
[textLayer setFontSize:17.0];
textLayer.masksToBounds = YES;
textLayer.position = CGPointMake(CGRectGetMidX(view.bounds), CGRectGetMidY(view.bounds));
[view.layer addSublayer:textLayer];
Swift 3 version for regular and attributed strings.
class ECATextLayer: CATextLayer {
override open func draw(in ctx: CGContext) {
let yDiff: CGFloat
let fontSize: CGFloat
let height = self.bounds.height
if let attributedString = self.string as? NSAttributedString {
fontSize = attributedString.size().height
yDiff = (height-fontSize)/2
} else {
fontSize = self.fontSize
yDiff = (height-fontSize)/2 - fontSize/10
}
ctx.saveGState()
ctx.translateBy(x: 0.0, y: yDiff)
super.draw(in: ctx)
ctx.restoreGState()
}
}
thank #iamktothed, it works. following is swift 3 version:
class CXETextLayer : CATextLayer {
override init() {
super.init()
}
override init(layer: Any) {
super.init(layer: layer)
}
required init(coder aDecoder: NSCoder) {
super.init(layer: aDecoder)
}
override func draw(in ctx: CGContext) {
let height = self.bounds.size.height
let fontSize = self.fontSize
let yDiff = (height-fontSize)/2 - fontSize/10
ctx.saveGState()
ctx.translateBy(x: 0.0, y: yDiff)
super.draw(in: ctx)
ctx.restoreGState()
}
}
There is nothing stopping you from creating a CALayer hierarchy with a generic CALayer (container) that has the CATextLayer as a sublayer.
Instead of calculating font sizes for the CATextLayer, simply calculate the offset of the CATextLayer inside the CALayer so that it is vertically centred. If you set the alignment mode of the text layer to centred and make the width of the text layer the same as the enclosing container it also centres horizontally.
let container = CALayer()
let textLayer = CATextLayer()
// create the layer hierarchy
view.layer.addSublayer(container)
container.addSublayer(textLayer)
// Setup the frame for your container
...
// Calculate the offset of the text layer so that it is centred
let hOffset = (container.frame.size.height - textLayer.frame.size.height) * 0.5
textLayer.frame = CGRect(x:0.0, y: hOffset, width: ..., height: ...)
The sublayer frame is relative to its parent, so the calculation is fairly straightforward. No need to care at this point about font sizes. That's handled by your code dealing with the CATextLayer, not in the layout code.
Updating this thread (for single and multi line CATextLayer), combining some answers above.
class VerticalAlignedTextLayer : CATextLayer {
func calculateMaxLines() -> Int {
let maxSize = CGSize(width: frame.size.width, height: CGFloat(Float.infinity))
let font = UIFont(descriptor: self.font!.fontDescriptor, size: self.fontSize)
let charSize = font.lineHeight
let text = (self.string ?? "") as! NSString
let textSize = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
let linesRoundedUp = Int(ceil(textSize.height/charSize))
return linesRoundedUp
}
override func draw(in context: CGContext) {
let height = self.bounds.size.height
let fontSize = self.fontSize
let lines = CGFloat(calculateMaxLines())
let yDiff = (height - lines * fontSize) / 2 - lines * fontSize / 10
context.saveGState()
context.translateBy(x: 0, y: yDiff) // Use -yDiff when in non-flipped coordinates (like macOS's default)
super.draw(in: context)
context.restoreGState()
}
}
gbk's code works. below is gbk's code updated for XCode 8 beta 6. Current as of 1 Oct 2016
Step 1. Subclass CATextLayer. In the code below I've named the subclass "MyCATextLayer" Outside your view controller class copy/paste the below code.
class MyCATextLayer: CATextLayer {
// REF: http://lists.apple.com/archives/quartz-dev/2008/Aug/msg00016.html
// CREDIT: David Hoerl - https://github.com/dhoerl
// USAGE: To fix the vertical alignment issue that currently exists within the CATextLayer class. Change made to the yDiff calculation.
override init() {
super.init()
}
required init(coder aDecoder: NSCoder) {
super.init(layer: aDecoder)
}
override func draw(in ctx: CGContext) {
let height = self.bounds.size.height
let fontSize = self.fontSize
let yDiff = (height-fontSize)/2 - fontSize/10
ctx.saveGState()
ctx.translateBy(x: 0.0, y: yDiff)
super.draw(in: ctx)
ctx.restoreGState()
}
}
Step 2. Within your view controller class in your ".swift" file, create your CATextLabel. In the code example I've named the subclass "MyDopeCATextLayer."
let MyDopeCATextLayer: MyCATextLayer = MyCATextLayer()
Step 3. Set your new CATextLayer with desired text/color/bounds/frame.
MyDopeCATextLayer.string = "Hello World" // displayed text
MyDopeCATextLayer.foregroundColor = UIColor.purple.cgColor //color of text is purple
MyDopeCATextLayer.frame = CGRect(x: 0, y:0, width: self.frame.width, height: self.frame.height)
MyDopeCATextLayer.font = UIFont(name: "HelveticaNeue-UltraLight", size: 5) //5 is ignored, set actual font size using ".fontSize" (below)
MyDopeCATextLayer.fontSize = 24
MyDopeCATextLayer.alignmentMode = kCAAlignmentCenter //Horizontally centers text. text is automatically centered vertically because it's set in subclass code
MyDopeCATextLayer.contentsScale = UIScreen.main.scale //sets "resolution" to whatever the device is using (prevents fuzzyness/blurryness)
Step 4. done
The code for Swift 3, based on code #iamktothed
If you use an attributed string for setting font properties, than you can use function size() from NSAttributedString to calculate height of string.
I think this code also resolve the problems described by #Enix
class LCTextLayer: CATextLayer {
override init() {
super.init()
}
override init(layer: Any) {
super.init(layer: layer)
}
required init(coder aDecoder: NSCoder) {
super.init(layer: aDecoder)
}
override open func draw(in ctx: CGContext) {
if let attributedString = self.string as? NSAttributedString {
let height = self.bounds.size.height
let stringSize = attributedString.size()
let yDiff = (height - stringSize.height) / 2
ctx.saveGState()
ctx.translateBy(x: 0.0, y: yDiff)
super.draw(in: ctx)
ctx.restoreGState()
}
}
}
I slightly modified this answer by #iamkothed. The differences are:
text height calculation is based on NSString.size(with: Attributes). I don't know if it's an improvement over (height-fontSize)/2 - fontSize/10, but I like to think that it is. Although, in my experience, NSString.size(with: Attributes) doesn't always return the most appropriate size.
added invertedYAxis property. It was useful for my purposes of exporting this CATextLayer subclass using AVVideoCompositionCoreAnimationTool. AVFoundation operates in "normal" y axis, and that's why I had to add this property.
Works only with NSString. You can use Swift's String class though, because it automatically casts to NSString.
It ignores CATextLayer.fontSize property and completely relies on CATextLayer.font property which MUST be a UIFont instance.
class VerticallyCenteredTextLayer: CATextLayer {
var invertedYAxis: Bool = true
override func draw(in ctx: CGContext) {
guard let text = string as? NSString, let font = self.font as? UIFont else {
super.draw(in: ctx)
return
}
let attributes = [NSAttributedString.Key.font: font]
let textSize = text.size(withAttributes: attributes)
var yDiff = (bounds.height - textSize.height) / 2
if !invertedYAxis {
yDiff = -yDiff
}
ctx.saveGState()
ctx.translateBy(x: 0.0, y: yDiff)
super.draw(in: ctx)
ctx.restoreGState()
}
}
class CenterTextLayer: CATextLayer {
override func draw(in ctx: CGContext) {
#if os(iOS) || os(tvOS)
let multiplier = CGFloat(1)
#elseif os(OSX)
let multiplier = CGFloat(-1)
#endif
let yDiff = (bounds.size.height - ((string as? NSAttributedString)?.size().height ?? fontSize)) / 2 * multiplier
ctx.saveGState()
ctx.translateBy(x: 0.0, y: yDiff)
super.draw(in: ctx)
ctx.restoreGState()
}
}
Credit goes to:
https://github.com/cemolcay/CenterTextLayer
So there is no "direct" way of doing this but you can accomplish the same thing by using text metrics:
http://developer.apple.com/library/ios/#documentation/UIKit/Reference/NSString_UIKit_Additions/Reference/Reference.html
... for example, find the size of the text then use that information to place it where you want in the parent layer. Hope this helps.
You need to know where CATextLayer will put the baseline of your text. Once you know that, offset the coordinate system within the layer, i.e. adjust bounds.origin.y by the difference between where the baseline normally sits and where you want it to be, given the metrics of the font.
CATextLayer is a bit of a black box and finding where the baseline will sit is a bit tricky - see my answer here for iOS - I've no idea what the behaviour is on Mac.
I'd like to propose a solution that takes multiline wrapping inside the available box into account:
final class CACenteredTextLayer: CATextLayer {
override func draw(in ctx: CGContext) {
guard let attributedString = string as? NSAttributedString else { return }
let height = self.bounds.size.height
let boundingRect: CGRect = attributedString.boundingRect(
with: CGSize(width: bounds.width,
height: CGFloat.greatestFiniteMagnitude),
options: NSString.DrawingOptions.usesLineFragmentOrigin,
context: nil)
let yDiff: CGFloat = (height - boundingRect.size.height) / 2
ctx.saveGState()
ctx.translateBy(x: 0.0, y: yDiff)
super.draw(in: ctx)
ctx.restoreGState()
}
}
Swift 5.3 for macOS
class VerticallyAlignedTextLayer : CATextLayer {
/* Credit - purebreadd - 6/24/2020
https://stackoverflow.com/questions/4765461/vertically-align-text-in-a-catextlayer
*/
func calculateMaxLines() -> Int {
let maxSize = CGSize(width: frame.size.width, height: frame.size.width)
let font = NSFont(descriptor: self.font!.fontDescriptor, size: self.fontSize)
let charSize = floor(font!.capHeight)
let text = (self.string ?? "") as! NSString
let textSize = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font!], context: nil)
let linesRoundedUp = Int(floor(textSize.height/charSize))
return linesRoundedUp
}
override func draw(in context: CGContext) {
let height = self.bounds.size.height
let fontSize = self.fontSize
let lines = CGFloat(calculateMaxLines())
let yDiff = -(height - lines * fontSize) / 2 - lines * fontSize / 6.5 // Use -(height - lines * fontSize) / 2 - lines * fontSize / 6.5 when in non-flipped coordinates (like macOS's default)
context.saveGState()
context.translateBy(x: 0, y: yDiff)
super.draw(in: context)
context.restoreGState()
}
}
Notice I am dividing fontSize by 6.5, which seems to work better for my application of this solution. Thanks #purebreadd!
As best I can tell, the answer to my question is "No."