Nicely Formatted Lists with ListFormatter

Last time we took a look at the very useful RelativeDateTimeFormatter to create localized strings describing date differences programmatically. In this post we’ll look at a better way to handle presenting lists to your users. As programmers, it’s pretty common to have to take a list of objects (e.g. apples, bananas, oranges) and express the list as a string (e.g. “apples, bananas, and oranges”). Doing so however, can often involve some ugly code. Let’s say we just wanted to do the above before iOS 13. We might have done something like:

var list = ["apples", "bananas", "oranges"]
var output = ""
for (index, item) in list.enumerated() {
    output.append(item)
    if index < list.count - 2 {
        //Normal separator
        output.append(", ")
    } else if index < list.count - 1 {
        //Final separator
        output.append(", and ")
    }
}
print(output)

The above is not pretty (and, yes, there are a million ways to do this), but more importantly it’s not easy to localize this, deal with linguistic idiosynchronicities, or even right-to-left formatting in Arabic. With iOS 13, Apple also introduced an incredibly useful tool to format lists that can help with all of this, the ListFormatter class!

Simple List Formatting

As with most of the classes derived from Formatter, creating and outputting a string with the ListFormatter is fairly simple.

  1. Create the formatter
  2. Set some properties on the formatter
  3. Call the appropriate string(for:) function

For example, to create a simple list of localized strings, you could simply do the following:

var fruits = [
    NSLocalizedString("apples", comment: "red or green fruit"),
    NSLocalizedString("bananas", comment: "long yellow fruit"),
    NSLocalizedString("oranges", comment: "round orange fruit")
]
//Create the formatter
let formatter = ListFormatter()
//Set the locale as appropriate
formatter.locale = Locale(identifier: "en_US")
//Get the string
print(formatter.string(for: fruits)!)

//"apples, bananas, and oranges"

If you have resource files set up with the appropriate localized strings, that’s all that’s needed. It doesn’t matter how long the list is, what it contains, etc. Pretty simple, huh? We can make the above example even simpler though with the localizedString(byJoining:) property on ListFormatter which is static and skips the formatter creation steps in the example above.

//One easy step without localized strings set up individually
var fruits = ["apples", "bananas", "oranges"]
print(ListFormatter.localizedString(byJoining: fruits))

//"apples, bananas, and oranges"

Now in this example, you don’t have the control of NSLocalizedString for creating comments, etc. but the syntax is much more succinct. This method also uses the locale of the current context and so may not be as flexible as needed.

Getting Even More out of Formatted Lists

Let’s say that you have a list of dates or numbers and these need to be output into a nicely formatted string across multiple locales. Before ListFormatter, you would have to consider writing a ton of code to first format each individual item in the list and then loop through and create the list. And even then, localizing properly across the entire globe would be extremely difficult if not near impossible to do 100% correctly. Once again, enter the ListFormatter!

Let’s say that you’ve got a set of prices that need to be output in a formatted list for a brochure or similar.

//Prices for each widget
var priceList: [Double] = [10, 100, 500, 1000]

All we have do now is generate or use a ListFormatter and pass the appropriate formatter to the itemFormatter property. This could be a DateFormatter, NumberFormatter or even your own custom implementation of the Formatter class. So first we’ll create the item formatter:

//Create the item formatter.  Don't forget to set the locale here as well
let priceFormatter = NumberFormatter()
priceFormatter.locale = Locale(identifier: localeIdentifier)
priceFormatter.numberStyle = .currency

Then we just need to create the list formatter and assign this formatter:

//Create list formatter and assign the item formatter to the list formatter
let formatter = ListFormatter()
formatter.itemFormatter = priceFormatter
formatter.locale = Locale(identifier: localeIdentifier)
return formatter.string(from: priceList)!

The list formatter controls the separators between the list items and the item formatter formats the items themselves. Lets look at the code above in a finished example of the above and examine what actually comes back.

//Prices for each widget
var prices: [Double] = [10, 100, 500, 1000]

/// Format price list
/// - Parameters:
///   - list:
///   - localeIdentifier: Locale identifier
/// - Returns: Price list all formatted
func formatList(_ prices: [Double], localeIdentifier: String) -> String {
    //Create the item formatter.  Don't forget to set the locale here as well
    let priceFormatter = NumberFormatter()
    priceFormatter.locale = Locale(identifier: localeIdentifier)
    priceFormatter.numberStyle = .currency
    
    //Create list formatter and assign the item formatter to the list formatter
    let formatter = ListFormatter()
    formatter.itemFormatter = priceFormatter
    formatter.locale = Locale(identifier: localeIdentifier)
    
    //Output the results or empty string
    guard let output = formatter.string(from: prices) else {
        return ""
    }
    return output
}

print(formatList(prices, localeIdentifier: "en_US"))  //English US
print(formatList(prices, localeIdentifier: "fr_FR"))  //French 
print(formatList(prices, localeIdentifier: "de_DE"))  //German
print(formatList(prices, localeIdentifier: "zh_CN"))  //Chinese
print(formatList(prices, localeIdentifier: "ar_AE"))  //Arabic

Output
--------------
$10.00, $100.00, $500.00, and $1,000.00
10,00 €, 100,00 €, 500,00 € et 1 000,00 €
10,00 €, 100,00 €, 500,00 € und 1.000,00 €
¥10.00、¥100.00、¥500.00和¥1,000.00
د.إ.‏ 10.00، د.إ.‏ 100.00، د.إ.‏ 500.00، ود.إ.‏ 1,000.00

Notice above that the currency symbols have been inserted as appropriate to each locale. Even more impressive, note that the list in Arabic has been properly output right-to-left.

Keep Going!

Hopefully the above will get you started generating some cool looking lists as well as save you from writing a bunch of messy, fragile boilerplate code. When you consider what auto-layout can do to word wrap your lists generated from ListFormatter, your user interfaces should look better than ever.

There are tons of possibilities with this formatter as well as all of the other formatters available through Foundation in iOS. The online documentation is still somewhat sparse when it comes to the new stuff online but the code comments around ListFormatter are actually fairly complete. Try writing your own custom formatter based on Formatter if there’s some object in your code that needs to be output to a String or AttributedString different ways. Combine that with ListFormatter for even more power!

Till next time!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: