A label has four different verticalAlignmentMode
: .Baseline
, .Bottom
, .Center
and .Top
.
I would like the label to be centred on its position according to its baseline. .Center
doesn't work for me because the bottom of the frame isn't the baseline of the text, but rather, the bottom of the lowest letter (like a 'y', for example).
I have also tried using .Baseline
and subtracting half the frame's height from the y-position, but this doesn't work either and results in the same problem as .Center
.
The text in the label I'm trying to centre is "Play!". Setting the mode to '.Center' makes the text slightly higher than what I want it to be which is quite noticeable. Changing the text to "Pla!" fixes the problem because the 'y' is removed and none of the characters hang below the baseline (I obviously can't do this as the solution, though).
I would like a suggestion for a way around this problem — perhaps there is some way to get the position of the baseline? Thanks!
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.