Variable Width Strings

Have you ever had the title text of your buttons and other controls truncated because they were too long to fit in the available space? This can be a problem on smaller devices and with localized text.

Wouldn’t it be useful if you could automatically change the text based on the available space? There’s a little-known feature of the localization system that does precisely that.

What’s The Problem?

Here’s an example with two equal width buttons arranged horizontally. The title text of the two buttons use the 17pt system font and just about fit in the available space running on an iPhone 8:

Buttons on iPhone 8

On a smaller device like the iPhone SE the button titles don’t fit and end up truncated:

Buttons on iPhone SE

If we were to localize the titles, we might end up with even longer text making the problem worse.

Localization Strings Dictionary

The localization system has a solution to this problem using a localizable strings dictionary. This allows us to build a dictionary of variable width strings. Standard UIKit components like UILabel and UIButton are aware of the variations and automatically choose the best string from the dictionary for the available screen width.

Let’s see how it works. Here’s my layout with the two buttons in a horizontal stack view. I pinned the stack view to the top, leading and trailing margins of the root view. The stack view is using a fill equally distribution and standard spacing:

Interface Builder layout

We have to adapt our strings in code, so we need outlets connected to the two buttons in our view controller:

@IBOutlet private var abortButton: UIButton!
@IBOutlet private var startButton: UIButton!

To create the variable width strings we need a localizable strings dictionary. Add a new file to the Xcode project and choose “Stringsdict File” from the template browser (you’ll find it in the iOS Resource section):

Add Stringsdict file

Name the file Localizable.stringsdict and save it in the base localization directory (Base.lproj) with the project storyboards.

The strings dictionary needs an item for each of the button titles that we want to localize. We use the key (Abort and Start) of the item when looking up the localization to set our button titles. Each string item is a dictionary containing another dictionary with width variations rules:

Strings dictionary

Each width variation entry has a width in em units and a string value. When looking up the localized string, we get back the variation that best fits the available width.

What’s An Em Unit?

An em is a typographical measure of the type size. So for a 17 point type, 1 em is a distance of 17 points. The name comes from the traditional use of the width of the capital M to approximate the type size.

Apple doesn’t document how UIKit calculates the em value. In WWDC 2015 Session 227 on internationalization, Apple says they calculate it from the number of “M” characters that fit in the visible screen width at the standard system font size (14 points).

Some experimentation gave me these em sizes for a selection of devices:

iPhone SE         22em x 39em
iPhone 8          25em x 46em
iPhone 8 Plus     28em x 50em
iPhone X/XS       25em x 56em
iPhone XR/XS Max  28em x 61em
iPad Pro 10.5     57em x 76em
iPad Pro 12.9     70em x 94em

You may need to experiment with the em values to use. I followed an Apple example and used 20, 25 and 50. The 20em variation only shows up on the iPhone SE. The 25em variation covers the other iPhones in portrait and 50em the larger iPhones in landscape and all iPads. You may also need to use different values for different localizations depending on the string length.

Using Localized Strings

To use our variable width strings, we need to manually set the button titles in our view controller when loading the view:

override func viewDidLoad() {
  super.viewDidLoad()
  setupView()
}

private func setupView() {
  let startTitle = NSLocalizedString("Start", comment: "Start button")
  startButton.setTitle(startTitle, for: .normal)

  let abortTitle = NSLocalizedString("Abort", comment: "Abort button")
  abortButton.setTitle(abortTitle, for: .normal)
}

We use NSLocalizedString to look up the localization based on the key name we used in the strings dictionary. There’s nothing more to do but build and run. The standard UIKit controls like UILabel and UIButton are smart enough to know about variable width strings and apply the appropriate localization based on the screen size at runtime.

Here’s how it looks when running on the iPhone SE which uses the 20em string variation:

iPhone SE

Running on the iPhone 8 in portrait we get the 25em variation:

iPhone 8

On an iPhone XR in landscape gives us the 50em variation:

iPhone XR

I should say, I’ve seen situations where the text does not always adapt to width changes at runtime. You sometimes get the shorter string when a longer one would fit. For example, rotating the iPhone SE simulator from portrait to landscape does not switch from the 20em to the 25em variation.

Get The Code

You can download the code for this post from my GitHub Code Examples repository:

Want To Learn More?

If you enjoyed this post and want to learn more techniques for building adaptive layouts, you should get my book - Modern Auto Layout.