A label has four different verticalAlignmentMode
: .Baseline
, .Bottom
, .Center
and .Top
.
I would like
So I've had the same issue and with the help with of this question and answer I found a decent solution.
I wanted to align the text of label nodes with custom button nodes (a label node wrapped in a shape node to draw the outline).
I also wanted to align toggle (on/off) buttons with the baseline of the label node and use the same height as the xHeight of the text in the label.
The red lines show that all labels & buttons are properly aligned.
So in order to do this, I've done the following.
I created a Font class that can be used to create a font object from the fontName
and fontSize
in a SKLabelNode
.
Internally it will create either a NSFont
(macOS) or UIFont
(iOS). And set the ascender, descender properties. For convenience there's also a getter that returns the maximum possible height of the font.
import Foundation
#if os(macOS)
import Cocoa
#endif
#if os(iOS)
import UIKit
#endif
enum FontError: LocalizedError {
case invalidFont(String, CGFloat)
var errorDescription: String? {
switch self {
case .invalidFont(let name, let size):
return "Could not create font with name \(name) and size: \(size)"
}
}
}
class Font {
private(set) var name: String
private(set) var size: CGFloat
private(set) var ascender: CGFloat
private(set) var descender: CGFloat
private(set) var xHeight: CGFloat
var maxHeight: CGFloat {
return self.ascender + fabs(self.descender)
}
init(name: String, size: CGFloat) throws {
// check if font can be created, otherwise crash
// set ascender, descender, etc...
#if os(macOS)
guard let font = NSFont(name: name, size: size) else {
throw FontError.invalidFont(name, size)
}
self.ascender = font.ascender
self.descender = font.descender
self.xHeight = font.xHeight
#endif
#if os(iOS)
guard let font = UIFont(name: name, size: size) else {
throw FontError.invalidFont(name, size)
}
self.ascender = font.ascender
self.descender = font.descender
self.xHeight = font.xHeight
#endif
self.name = name
self.size = size
}
}
I then use the maxHeight in my label position calculations. So for example in my custom ButtonNode
(a SKShapeNode
that contains a SKLabelNode
) I do the following (simplified):
// in the constructor of my custom
// button node ...
let height = 30
self.label = SKLabelNode(text: "Back")
// in order to position properly, set
// vertical alignment to baseline
self.label.verticalAlignmentMode = .baseline
let font = try Font(name: self.label.fontName!, size: self.label.fontSize)
// depending on the font you might want to add
// an additional y offset to place labels
// higher or lower within the node
let yOffset = (height - font.maxHeight) / 2)
let labelWidth = self.label.calculateAccumulatedFrame().width
let labelFrame = CGRect(
x: 0,
y: 0,
width: labelWidth,
height: height
)
self.path = CGPath(roundedRect: labelFrame, cornerWidth: 5, cornerHeight: 5, transform: nil)
self.label.position = CGPoint(x: labelFrame.width / 2, y: yOffset)
P.S.: If you prefer to not create a separate Font
class and you still want the code to work properly on iOS and macOS, you could just create an typealias for NSFont
and UIFont
. Since you can use the same constructor, you'd only need to add an extension to calculate the max height of the font (ascender + abs(descender)
).
I prefer to create a class so I can throw an error if the font could not be created properly.