Writing in NSView - swift

I am trying to render text in a NSView canvas. I need to write three lines of text and ignore what's beyond. String.draw(in:withAttributes) with a provided rect seems perfect to do it. My code looks like this:
func renderText(_ string:String, x:Double, y:Double, numberOfLines: Int, withColor color:Color) -> Double {
let font = NSFont.boldSystemFont(ofSize: 11)
let lineHeight = Double(font.ascender + abs(font.descender) + font.leading)
let textHeight = lineHeight * Double(numberOfLines) + font.leading // three lines
let textRect = NSRect(x: x, y: y, width: 190, height: textHeight)
string.draw(in: textRect, withAttributes: [NSFontAttributeName: font, NSForegroundColorAttributeName: color])
return textHeight
}
renderText("Lorem ipsum...", x: 100, y: 100, numberOfLines: 3, withColor: NSColor.white)
Without adjustments, I get only two lines of text rendered:
I am following these guidelines: https://developer.apple.com/library/content/documentation/TextFonts/Conceptual/CocoaTextArchitecture/FontHandling/FontHandling.html#//apple_ref/doc/uid/TP40009459-CH5-SW18
I am missing something?

Ultimately your text makes it to the screen by calling upon the classes that comprise Cocoa's text architecture, so it makes sense to get information about line height directly from these classes. In the code below I've created an NSLayoutManager instance, and set its typesetter behaviour property to match the value of the typesetter that is ultimately used by the machinery created by the function drawInRect:withAttributes:. Calling the layout manager's defaultLineHeight method then gives you the height value you're after.
lazy var layoutManager: NSLayoutManager = {
var layoutManager = NSLayoutManager()
layoutManager.typesetterBehavior = .behavior_10_2_WithCompatibility
return layoutManager
}()
func renderText(_ string:String, x:Double, y:Double, numberOfLines: Int, withColor color:NSColor) -> Double {
let font = NSFont.boldSystemFont(ofSize: 11)
let textHeight = Double(layoutManager.defaultLineHeight(for: font)) * Double(numberOfLines)
let textRect = NSRect(x: x, y: y, width: 190, height: textHeight)
string.draw(in: textRect, withAttributes: [NSFontAttributeName: font, NSForegroundColorAttributeName: color])
return textHeight
}

You are writing your text into a precise typographic bounds, but the system may adjust the size of the text for on-screen presentation to make the text more legible (e.g. substitute screen fonts for for vector fonts). Using an exact typographic bounds can also cause problems for characters with ascenders or descenders that fall outside of bounds. For example the "A-ring" character or a capital E with an grave accent.
To find the bounds of text using rules of the CGContext that it will be drawn in I suggest boundingRectWithSize:options:context: (for NSAttributedString) and boundingRectWithSize:options:attributes:context: (for NSString)

I would try adding a small delta to textHeight -- Doubles are perfectly accurate.

Related

Why does drawing using CoreText require increasing the size of the CGRect?

I'm trying to draw text using CoreText. I have the following code:
guard let context = UIGraphicsGetCurrentContext() else { return }
let attrString = NSAttributedString(string: "Str")
let path = CGMutablePath()
// Commented code does work, why?
// path.addRect(CGRect(x: 0, y: 50, width: attrString.size().width + 1, height: attrString.size().height + 1))
path.addRect(CGRect(x: 0, y: 50, width: attrString.size().width, height: attrString.size().height))
let framesetter = CTFramesetterCreateWithAttributedString(attrString as CFAttributedString)
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrString.length), path, nil)
CTFrameDraw(frame, context)
Why does increasing the size of CGRect help, while the actual size of NSAttributedString doesn't?
Any help is appreciated.
When working with an attributed string and getting its size, you need to at least specify a font. The size of "Str" will be quite different with an 8 point font versus a 72 point font, for example.
It might also help to convert the calculated CGRect into an integral rect which will round up any fractional sizes. Use the integral property of CGRect.

NSAttributedString boundingRect returns wrong height

I calculate the height of an NSAttributedString like this.
let maxSize = NSSize(width: w, height: CGFloat.greatestFiniteMagnitude)
let rect = boundingRect(with: maxSize, options: [.usesFontLeading, .usesLineFragmentOrigin])
let height = rect.integral.size.height
I tried about every "hack" that was mentioned on SO, yet the string height gets more inaccurate the smaller the width gets (the calculated height is larger than the actual height).
According to other posts the following things cause problems with size calculation:
Whitespace
No foreground color
No background color
Widths below 300 don't work (?)
I have found that none of these suggestions make any difference. The attributed string is a concatenation of lines, each having a foregroundColor and backgroundColor. The string also has the following paragraph style.
let pstyle = NSMutableParagraphStyle()
pstyle.lineSpacing = 6
pstyle.lineBreakMode = .byWordWrapping
and a userFixedPitchFont of size 11.
Why does the height error get larger the smaller the width is?
PS: I imagine it has something to do with lineBreak since the error gets larger if more lines are word wrapped.
I found that this solution works. Note that if you don't set the lineFragmentPadding to 0, it produces the same (wrong) results as boundingRect.
extension NSAttributedString {
func sizeFittingWidth(_ w: CGFloat) -> CGSize {
let textStorage = NSTextStorage(attributedString: self)
let size = CGSize(width: w, height: CGFloat.greatestFiniteMagnitude)
let boundingRect = CGRect(origin: .zero, size: size)
let textContainer = NSTextContainer(size: size)
textContainer.lineFragmentPadding = 0
let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
layoutManager.glyphRange(forBoundingRect: boundingRect, in: textContainer)
let rect = layoutManager.usedRect(for: textContainer)
return rect.integral.size
}
}

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.

Different and inconsistent line heights in NSTextView and CATextLayer

I am working on creating a canvas-like interface where text box objects can be created and edited. All drawing is handled through CALayers, but I am using invisible NSTextViews for event handling when a text box is being actively edited. For accurate event capture, I need the lines of text in the NSTextView and CATextLayer to be laid out identically (particularly for multi-line text boxes).
The problem is that NSTextView and CATextLayer seem to render text slightly differently, giving them different line heights. This varies by font; sometimes NSTextView comes up shorter, sometimes longer. The black text below is in an NSTextView, while the blue is a CATextLayer. Helvetica Neue is slightly misaligned as lines accumulate, while Helvetica is quite overtly misaligned from the start.
I have no idea where the difference comes from, especially since the effect is so unpredictable. Is there a way to account for the line height difference, or, is there a better way to relay events captured from an NSTextView to/from a CATextLayer?
Swift playground code (macOS 10.12.1, Xcode 7.1.1; though the issue has existed since at least 10.11):
// Tested fonts:
// Andale Mono - slight misalignment
// Courier - extreme misalignment
// Courier New - small misalignment
// Helvetica - large misalignment
// Helvetica Neue - no (?) misalignment
import Cocoa
// setup
var str = "Text\nText\nText\nText\nText\nText\nText\nText\nText"
var theFont = CTFontCreateWithName("Helvetica Neue", 20, nil)
var parentView:NSView = NSView(frame: NSRect(origin: CGPointZero, size: CGSize(width: 300, height: 300)))
// create basic NSTextView
var textView:NSTextView = NSTextView(frame: NSRect(origin: CGPointZero, size: CGSize(width: 100, height: 300)))
textView.string = str
textView.font = theFont
parentView.addSubview(textView)
// create what SHOULD be an equivalent CATextLayer
var layerView:NSView = NSView(frame: NSRect(origin: CGPoint(x: 100, y: 0), size: CGSize(width: 100, height: 300)))
layerView.wantsLayer = true
var textLayer = CATextLayer()
textLayer.contentsScale = 2
textLayer.backgroundColor = NSColor(calibratedHue: 0, saturation: 0, brightness: 1, alpha: 1).CGColor
textLayer.foregroundColor = NSColor(calibratedHue: 0.6, saturation: 1, brightness: 1, alpha: 1).CGColor
textLayer.string = str
textLayer.font = CTFontCopyFullName(theFont)
textLayer.fontSize = CTFontGetSize(theFont)
layerView.layer = textLayer
parentView.addSubview(layerView)
// final display
parentView

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."