I am working on building a PDF document and the images and texts are being written to a view as CALayers. I am needing to vertically center my text within the bounding frame of a CATextLayer. I am using a class I found from 2016 as shown below that overrides the draw function. I was wondering if there are any new tricks to make this work?
As you can see when you run this code the text for cell 2 is not even being displayed, and cell 3 text is not being vertically centered.
Massive thanks to anyone who can help me.
//
// ViewController.swift
// CALayers Example
//
// Created by Thomas Carroll on 8/18/20.
// Copyright © 2020 Thomas Carroll. All rights reserved.
//
import Cocoa
class ViewController: NSViewController {
let myLayers = MyLayers()
override func viewDidLoad() {
super.viewDidLoad()
self.view.wantsLayer = true
self.view.layer?.addSublayer(myLayers.insertGrid())
self.view.layer?.addSublayer(myLayers.insertText())
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
//
// MyLayers.swift
// CALayers Example
//
// Created by Thomas Carroll on 8/18/20.
// Copyright © 2020 Thomas Carroll. All rights reserved.
//
import Cocoa
// Set up constant variables
let pageWidth:Float = 72*8.5
let pageHeight:Float = 72*11
// Set up coordinates
let leftX = Int(pageWidth/2-72*2.5)
let col1X = Int(leftX+72)
let col2X = Int(col1X+72)
let col3X = Int(col2X+72)
let col4X = Int(col3X+72)
let rightX = Int(col4X+72)
let bottomY = Int(pageHeight/2-72*2.5)
let row4Y = Int(bottomY+72)
let row3Y = Int(row4Y+72)
let row2Y = Int(row3Y+72)
let row1Y = Int(row2Y+72)
let topY = Int(row1Y+72)
// Set the extension to draw Bezier paths into a CAShapeLayer
extension NSBezierPath {
// Credit - Henrick - 9/18/2016
// https://stackoverflow.com/questions/1815568/how-can-i-convert-nsbezierpath-to-cgpath
public var cgPath: CGPath {
let path = CGMutablePath()
var points = [CGPoint](repeating: .zero, count: 3)
for i in 0 ..< self.elementCount {
let type = self.element(at: i, associatedPoints: &points)
switch type {
case .moveTo:
path.move(to: points[0])
case .lineTo:
path.addLine(to: points[0])
case .curveTo:
path.addCurve(to: points[2], control1: points[0], control2: points[1])
case .closePath:
path.closeSubpath()
@unknown default:
print("Error occured in NSBezierPath extension.")
}
}
return path
}
}
class MyLayers {
class VerticallyAlignedTextLayer : CATextLayer {
func calculateMaxLines() -> Int {
let maxSize = CGSize(width: frame.size.width, height: frame.size.height)
let font = NSFont(descriptor: self.font!.fontDescriptor, size: self.fontSize)
let charSize = (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(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()
}
}
func insertGrid() -> CALayer {
/*
Draws a single table grid of 25 boxes (5 high by 5 wide)
centered on a letter sized page
*/
// Create a new shape layer for the grid
let gridLayer = CAShapeLayer()
// Create the path
let gridPath = NSBezierPath()
// Assign the grid fill and stroke colors
gridLayer.strokeColor = NSColor.purple.cgColor
gridLayer.fillColor = NSColor.clear.cgColor
// Draw the paths for the grid
// Create the outside box
gridPath.move(to: CGPoint(x: leftX, y: bottomY)) // Bottom left corner
gridPath.line(to: CGPoint(x: leftX, y: topY)) // Column 1, left line
gridPath.line(to: CGPoint(x: rightX, y: topY)) // Row 1, top line
gridPath.line(to: CGPoint(x: rightX, y: bottomY)) // Column 5 right line
gridPath.line(to: CGPoint(x: leftX, y: bottomY)) // Row 5 bottom line
// Add in column lines
gridPath.move(to: CGPoint(x: col1X, y: topY)) // Between columns 1 & 2
gridPath.line(to: CGPoint(x: col1X, y: bottomY)) // Line between columns 1 & 2
gridPath.move(to: CGPoint(x: col2X, y: topY)) // Between columns 2 & 3
gridPath.line(to: CGPoint(x: col2X, y: bottomY)) // Line between columns 2 & 3
gridPath.move(to: CGPoint(x: col3X, y: topY)) // Between columns 3 & 4
gridPath.line(to: CGPoint(x: col3X, y: bottomY)) // Line between columns 3 & 4
gridPath.move(to: CGPoint(x: col4X, y: topY)) // Between columns 4 & 5
gridPath.line(to: CGPoint(x: col4X, y: bottomY)) // Line between columns 4 & 5
// Add in row lines
gridPath.move(to: CGPoint(x: leftX, y: row1Y)) // Between rows 1 & 2
gridPath.line(to: CGPoint(x: rightX, y: row1Y)) // Line between rows 1 & 2
gridPath.move(to: CGPoint(x: leftX, y: row2Y)) // Between rows 2 & 3
gridPath.line(to: CGPoint(x: rightX, y: row2Y)) // Line between rows 2 & 3
gridPath.move(to: CGPoint(x: leftX, y: row3Y)) // Between rows 3 & 4
gridPath.line(to: CGPoint(x: rightX, y: row3Y)) // Line between rows 3 & 4
gridPath.move(to: CGPoint(x: leftX, y: row4Y)) // Between rows 4 & 5
gridPath.line(to: CGPoint(x: rightX, y: row4Y)) // Line between rows 4 & 5
// Close the path
gridPath.close()
// Add grid to layer (note the use of the cgPath extension)
gridLayer.path = gridPath.cgPath
return gridLayer
}
func insertText() -> CALayer {
// Create a CALayer to add the textLayer to
let myCALayer = CALayer()
// Set up an array to hold the x coordinate for each column
let colPosX = [leftX, col1X, col2X, col3X, col4X]
// Set up an array to hold the y coordinate for the first card
let rowPosY = [row1Y, row2Y, row3Y, row4Y, bottomY]
// Set some default text to be used in the textLayers
let cellText = ["This is some cell 1 text", "Cell 2 text", "This is text cell 3"]
for i in (0...2) {
let textLayer = VerticallyAlignedTextLayer()
textLayer.string = cellText[i]
textLayer.fontSize = 14
// Set the frame to be 1 pixel smaller than the grid cell to provide 1px padding
textLayer.frame = CGRect(origin: CGPoint(x: Int(colPosX[i])+1, y: Int(rowPosY[i])+1), size: CGSize(width: 70, height: 70))
textLayer.alignmentMode = .center
textLayer.isWrapped = true
textLayer.foregroundColor = NSColor.black.cgColor
textLayer.backgroundColor = NSColor.clear.cgColor
textLayer.truncationMode = .none
myCALayer.addSublayer(textLayer)
}
return myCALayer
}
}
Swift 5.3
Okay, I finally figured out how to do what I needed. Specifically, I needed to change the fontSize of the textLayer so it would fit within the bounding box horizontally and vertically while also being vertically centered within the bounding box. To do this, I found some code that checks a boundingRect of an attributed string. Also note that I changed the yDiff calculation where the fontSize is being divided by 6.5 rather than 10 and it positions the text better vertically for me.
class MyLayers {
class VerticallyAlignedTextLayer : 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()
}
}
func insertGrid() -> CALayer {
/*
Draws a single table grid of 25 boxes (5 high by 5 wide)
centered on a letter sized page
*/
// Create a new shape layer for the grid
let gridLayer = CAShapeLayer()
// Create the path
let gridPath = NSBezierPath()
// Assign the grid fill and stroke colors
gridLayer.strokeColor = NSColor.purple.cgColor
gridLayer.fillColor = NSColor.white.cgColor
// Draw the paths for the grid
// Create the outside box
gridPath.move(to: CGPoint(x: leftX, y: bottomY)) // Bottom left corner
gridPath.line(to: CGPoint(x: leftX, y: topY)) // Column 1, left line
gridPath.line(to: CGPoint(x: rightX, y: topY)) // Row 1, top line
gridPath.line(to: CGPoint(x: rightX, y: bottomY)) // Column 5 right line
gridPath.line(to: CGPoint(x: leftX, y: bottomY)) // Row 5 bottom line
//gridPath.close()
//gridLayer.path = gridPath.cgPath
// Add in column lines
gridPath.move(to: CGPoint(x: col1X, y: topY)) // Between columns 1 & 2
gridPath.line(to: CGPoint(x: col1X, y: bottomY)) // Line between columns 1 & 2
gridPath.move(to: CGPoint(x: col2X, y: topY)) // Between columns 2 & 3
gridPath.line(to: CGPoint(x: col2X, y: bottomY)) // Line between columns 2 & 3
gridPath.move(to: CGPoint(x: col3X, y: topY)) // Between columns 3 & 4
gridPath.line(to: CGPoint(x: col3X, y: bottomY)) // Line between columns 3 & 4
gridPath.move(to: CGPoint(x: col4X, y: topY)) // Between columns 4 & 5
gridPath.line(to: CGPoint(x: col4X, y: bottomY)) // Line between columns 4 & 5
// Add in row lines
gridPath.move(to: CGPoint(x: leftX, y: row1Y)) // Between rows 1 & 2
gridPath.line(to: CGPoint(x: rightX, y: row1Y)) // Line between rows 1 & 2
gridPath.move(to: CGPoint(x: leftX, y: row2Y)) // Between rows 2 & 3
gridPath.line(to: CGPoint(x: rightX, y: row2Y)) // Line between rows 2 & 3
gridPath.move(to: CGPoint(x: leftX, y: row3Y)) // Between rows 3 & 4
gridPath.line(to: CGPoint(x: rightX, y: row3Y)) // Line between rows 3 & 4
gridPath.move(to: CGPoint(x: leftX, y: row4Y)) // Between rows 4 & 5
gridPath.line(to: CGPoint(x: rightX, y: row4Y)) // Line between rows 4 & 5
// Close the path
gridPath.close()
// Add grid to layer (note the use of the cgPath extension)
gridLayer.path = gridPath.cgPath
return gridLayer
}
func sizeOfRect(string: NSString, fontSize: CGFloat) -> Int {
/* Credit to Jake Marsh - 12/10/2015
https://littlebitesofcocoa.com/144-drawing-multiline-strings
Return the height of a boundingRect for a specified string at a specified fontSize
*/
let cellFontSize:CGFloat = fontSize
let cellFont:NSFont = NSFont.systemFont(ofSize: cellFontSize, weight: .regular)
let cellParagraphStyle = NSMutableParagraphStyle()
let cellTextAttributes = [NSAttributedString.Key.font: cellFont, NSAttributedString.Key.paragraphStyle: cellParagraphStyle]
let cellDrawingOptions: NSString.DrawingOptions = [
.usesLineFragmentOrigin, .usesFontLeading]
cellParagraphStyle.lineHeightMultiple = 1.0
cellParagraphStyle.lineBreakMode = .byWordWrapping
return Int(string.boundingRect(with: CGSize(width: 70, height: CGFloat.infinity), options: cellDrawingOptions, attributes: cellTextAttributes).height)
}
func insertText() -> CALayer {
// Create a CALayer to add the textLayer to
let myCALayer = CALayer()
// Set up an array to hold the x coordinate for each column
let colPosX = [leftX, col1X, col2X, col3X, col4X]
// Set up an array to hold the y coordinate for the first card
let rowPosY = [row1Y, row2Y, row3Y, row4Y, bottomY]
// Set some default text to be used in the textLayers
let cellText = ["This is some cell 1 text that is kind of long", "Cell 2 text", "This is text cell 3", "Some really really long text"]
for i in (0...3) {
// Create a vertically centered textLayer
let textLayer = VerticallyAlignedTextLayer()
// Set up the initial font size for the text
var fontSize:CGFloat = 14
// Assign a string to the textLayer
textLayer.string = cellText[i]
// Check the vertical hieght of a rectangle that would contain the text based on the current fontSize. If the text is taler than the specific box height, reduce the fontSize my a half point until it is within the specified height of the box.
while sizeOfRect(string: cellText[i] as NSString, fontSize: fontSize) > 68 {
fontSize -= 0.5
}
// Assign the adjusted fontSize to the textLayer
textLayer.fontSize = fontSize
// Set the frame to be 4 pixel smaller than the grid cell to provide 2px padding
textLayer.frame = CGRect(origin: CGPoint(x: Int(colPosX[i])+2, y: Int(rowPosY[i])+2), size: CGSize(width: 68, height: 68))
textLayer.alignmentMode = .center
textLayer.isWrapped = true
textLayer.foregroundColor = NSColor.black.cgColor
textLayer.backgroundColor = NSColor.clear.cgColor
textLayer.truncationMode = .none
myCALayer.addSublayer(textLayer)
}
return myCALayer
}
}