Why are iOS Monospaced fonts not behaving like that?

1.2k views Asked by At

I am using SpriteKit and have a timer in my iOS app rendered through an SKLabelNode and which is updated in the update function. I didn't want the digits moving left and right as the count increased, so I tried using Courier New. It worked much better than Avenir, but there are still subtle fluctuations in the width of some of the digits. (The 6 and the 9 are slightly wider than the other digits.) I saw these fluctuations in several different experiments where I tried:

  • normal & bold styles
  • large (65) & small (9) font sizes
  • Courier New & Menlo fonts
  • Xcode simulator & iPod Touch

I submitted queries like "which font has uniform width digits", but all the answers I could find just say "use a monospaced font". (See https://graphicdesign.stackexchange.com/questions/13148/which-fonts-have-the-same-width-for-every-character or Monospace Font - Not really Monospace?)

My code is super simple. I setup the scoreNode once like this:

    scoreNode = SKLabelNode(fontNamed: "Courier New")
    scoreNode.fontSize = 65
    scoreNode.fontColor = UIColor.blackColor()
    scoreNode.text = "0"
    scoreNode.position = CGPoint(x: patternPane.midX, y: patternPane.midY)
    patternPaneNode.addChild(scoreNode)

And then I update the score like this:

    scoreNode.text = "\(viewControllerDelegate!.getScore())"

Here's an image overlaying the frame showing 28 with part of the frame showing 29. You can see that the edges of the movie are lined up and you can see that the base of the 2s do not line up.

partial 29 overlaid on 28

Any explanations?

Is it possible that iOS is applying kerning to the SKLabelNode even though I'm using a monospace font?

2

There are 2 answers

0
erhosen On

I faced the same problem and killed multiple days trying to solve it. Here is my solution:

So, first of all - the function that sets the score. It uses attributedText instead of just text. That made possible use of UIFont.monospacedSystemFont()

let labelFontSize = 17

func setScore(_ newScore: Int) {
    labelNode.attributedText = .init(
        string: String(newScore),
        attributes: [
            .font: UIFont.monospacedSystemFont(
                ofSize: labelFontSize,
                weight: .bold
            ),
            .foregroundColor: UIColor.white,
        ]
    )
}

Next - fix the position of SKLabelNode inside SKShapeNode (or any other rectangle):

func adjustLabelToFitRect(labelNode: SKLabelNode, rect: CGRect) {
    labelNode.verticalAlignmentMode = .baseline
    labelNode.horizontalAlignmentMode = .center

    // Originally this function worked with labelNode.frame.height
    // But it depends on fontSize, so I decided to use it as a base
    var labelHeightShift = (labelFontSize - 1) / 2.0
    labelHeightShift -= 0.8  // empirical pixel perfect observation

    // Move the SKLabelNode to the center of the rectangle.
    labelNode.position = CGPoint(x: rect.midX, y: rect.midY - labelHeightShift)
}

Note that we are setting verticalAlignmentMode to .baseline. Because of that, we need to calculate labelHeightShift by ourselves.

Yeah, the code is a little dirty with 0.8 and other constants. But I haven't found a better solution.

I hope that helps someone.

0
scribu On

Since iOS 9, all fonts use proportional numbers by default.

If you're using UILabel, you can opt into monospaced digits, but there isn't a solution for SKLabelNode, as far as I can tell.

More info in this WWDC2015 video (minute 26): https://developer.apple.com/videos/play/wwdc2015/804/