How combine lists of Strings into natural and localized sentences in Swift

Sponsored
RevenueCat logo

Helm Pro yearly subscribers now get a 30% discount on RocketSim thanks to contingent pricing on the App Store.

Swift is a powerful language that provides a bunch of built-in quality-of-life features that help developers write clean, efficient and readable code. The language keeps evolving with each new release and there are always new features to explore and learn about.

One such feature that only recently caught my attention is the ListFormatter class. If you have ever worked with lists in Swift and had to join them into a human-readable sentence, you might have used a combination of the joined operator and some custom logic to provide a different separator for the last item:

Joined.swift
let languages = ["Swift", "Kotlin", "Rust"]
let joinedLanguages = languages.dropLast().joined(separator: ", ")
    + (languages.count > 1 ? " and " : "")
    + (languages.last ?? "")
// "Swift, Kotlin and Rust"

While this implementation works for this specific use case, it does not scale well as it does not take into account the user’s locale and language.

This is where the ListFormatter and ListFormatStyle objects come in. They are a part of the Foundation framework and provide a way to join lists of items into a human-readable sentence that is both natural and localized.

Using ListFormatter

You can create a ListFormatter instance and then call the string(from:) method to convert any list of items into a human-readable sentence:

ListFormatter.swift
import Foundation

let listFormatter = ListFormatter()
// "Swift, Kotlin, and Rust"
listFormatter.string(from: ["Swift", "Kotlin", "Rust"])

By default, the ListFormatter class takes the user’s locale into consideration. You can also force a specific locale by modifying the locale property of the formatter’s instance:

ListFormatter.swift
import Foundation

let listFormatter = ListFormatter()
listFormatter.locale = Locale(identifier: "es-ES")
// "Swift, Kotlin y Rust"
listFormatter.string(from: ["Swift", "Kotlin", "Rust"])

It is important to note that, while I have only used arrays of Strings, the ListFormatter does not limit you to just that. In fact the API is designed to accept arrays of any type:

NSListFormatter.swift
import CoreFoundation

@available(macOS 10.15, *)
open class ListFormatter : Formatter {
    open func string(from items: [Any]) -> String?
}

While this is great, you must know that for the string method to yield a meaningful result, the items must be representable as a String. A lot of types, such as Ints are already representable as Strings, but for custom types, you might need to conform to the CustomStringConvertible protocol:

ListFormatter.swift
import Foundation

struct Language: CustomStringConvertible {
    let title: String
    
    var description: String {
        return title
    }
}

let listFormatter = ListFormatter()
// "Swift, Kotlin, and Rust"
listFormatter.string(from: [
    Language(title: "Swift"),
    Language(title: "Kotlin"),
    Language(title: "Rust")
])

Using the formatted list method

While ListFormatter works great, I prefer using the formatted method on a list instance as it is more concise and provides further customization options:

Formatted.swift
import Foundation

let languages = ["Swift", "Kotlin", "Rust"]
// "Swift, Kotlin, and Rust"
languages.formatted()
// "Swift, Kotlin, or Rust"
languages.formatted(.list(type: .or))
// "Swift, Kotlin, & Rust"
languages.formatted(.list(type: .and, width: .short))

As opposed to ListFormatter, the formatted method is limited to arrays of String instances.