Handling Non-Breaking Numbers in Dynamic Text


Greetings, traveler!

Displaying dynamically formatted numbers in user-facing strings is a common requirement in many iOS applications. Price labels, account balances, min/max amounts, and other numeric values often appear as part of longer sentences. While these strings usually render correctly, problems start to appear when a large number doesn’t fit on one line.

A typical example is a screen where the UI must adapt to both short and long numbers. If the number happens to be large enough, the system may break it across lines, leaving a portion on the previous line and the rest on the next. This creates an unpolished visual result and can make the number harder to read.

In cases where the number should always remain visually atomic—prices, sums of money, identifiers, or any numeric values formatted with grouping separators—we need to prevent the system from breaking it across lines. This is where non-breaking spaces become useful.

Why Numbers Break Across Lines

By default, text layout engines treat standard spaces as valid break points. A formatted number such as 12 345 678 contains regular spaces between digit groups. When the text no longer fits in the available width, the layout engine may insert a line break after any of these spaces, resulting in something like:

Minimum deposit amount is $12 345
678.

Although technically correct, this is undesirable from a UX standpoint. Users visually perceive a formatted number as a single unit, and splitting it harms readability.

Using Non-Breaking Spaces

A simple and reliable way to prevent this issue is to replace regular spaces inside the numeric value with non-breaking spaces (\u{00A0}). This instructs the layout engine to keep the entire number together on the same line and move it as a whole if necessary.

A small extension makes this easy to adopt across the codebase:

extension String {
    var nonBreakingSpaces: String {
        replacing(" ", with: "\u{00A0}")
    }
}

If your formatting logic produces values like "12 345 678", applying this modifier ensures the number will never be split during line wrapping.

Real-World Example

Consider a string used in banking or financial apps:

"The maximum loan amount you can request is %@."

The value is inserted dynamically and may vary widely. With large numbers, the default line wrapping can produce visually inconsistent layouts.

Applying the non-breaking space approach:

let formatted = minSum.formattedGrouped.nonBreakingSpaces
let text = String(format: "The maximum loan amount you can request is %@.", formatted)

Or you can use something like this:

import SwiftUI

// MARK: - Helpers

extension String {
    /// Replaces regular spaces with non-breaking spaces to keep the number on one line.
    var nonBreakingSpaces: String {
        replacingOccurrences(of: " ", with: "\u{00A0}")
    }
}

// Example formatter for grouped numbers
extension Int {
    var formattedGrouped: String {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        formatter.groupingSeparator = " "
        return formatter.string(from: NSNumber(value: self)) ?? "\(self)"
    }
}

// MARK: - View Example

struct ContentView: View {

    private let veryLargeNumber = 987_321_000

    var body: some View {
        VStack(alignment: .leading, spacing: 50) {
            Text("Total distance covered: \(veryLargeNumber.formattedGrouped.nonBreakingSpaces) km")
            
            Text("Total distance covered: \(veryLargeNumber.formattedGrouped) km")
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding()
        .fontWeight(.bold)
        .font(.title)
        .multilineTextAlignment(.leading)
    }
}

Conclusion

Non-breaking spaces provide a simple and effective way to preserve the integrity of numeric values in dynamic text.