Cache Date Formatters

ID

swift.cache_date_formatters

Severity

low

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Efficiency

Language

Swift

Tags

efficiency, performance

Description

Reports a Foundation formatter (DateFormatter, ISO8601DateFormatter, DateComponentsFormatter, NumberFormatter and friends) being constructed inside a loop body or a computed property’s getter.

for row in rows {
    let fmt = DateFormatter()                          // FLAW
    fmt.dateFormat = "yyyy-MM-dd"
    row.label = fmt.string(from: row.date)
}

struct View {
    var formatted: String {
        let fmt = ISO8601DateFormatter()               // FLAW
        return fmt.string(from: date)
    }
}

Rationale

Foundation formatters are surprisingly expensive to instantiate: constructing a DateFormatter parses calendar, locale and timezone data and locks an internal cache, often taking a millisecond or more even on modern devices. Inside a tight loop or a computed property that is read on every layout pass, the cost stacks up quickly and shows up in time-profiler traces.

Apple documents both DateFormatter and NumberFormatter as thread-safe for read access once configured, so a single shared instance is the idiomatic remedy.

Remediation

Hoist the formatter to a static or module-scope constant and configure it once:

private static let dayFormatter: DateFormatter = {
    let f = DateFormatter()
    f.dateFormat = "yyyy-MM-dd"
    f.locale = Locale(identifier: "en_US_POSIX")
    f.timeZone = TimeZone(identifier: "UTC")
    return f
}()

for row in rows {
    row.label = Self.dayFormatter.string(from: row.date)
}

If the formatter’s configuration depends on runtime state, cache the instance on the surrounding type and recompute only when the configuration changes.