Skip to content
macOSDeep Dive Published Updated 7 min readViews unavailable

ByteCountFormatter on macOS: Display Units, Rounding, and Exact Data Boundaries

Format byte counts on macOS with explicit binary or decimal units, localized display policies, exact integer storage, and boundary-focused validation.

A file inspector can show a plausible size while concealing two different decisions: how many bytes were measured and how those bytes were presented. Foundation’s ByteCountFormatter solves the presentation problem. It does not establish whether a count describes logical file length, allocated storage, compressed transfer size, or an estimate of remaining work.

Keep the underlying integer and its measurement meaning separate from the localized string. This distinction prevents rounded labels from becoming database values, binary and decimal units from being confused, and a user-interface change from altering a quota or billing calculation.

Define the measured quantity first

The formatter receives a count. A caller must supply the correct count for the product feature. Foundation exposes fileSize and fileAllocatedSize as separate file resource values: one is the total file size, while the other reports allocated on-disk bytes. The allocated value is based on blocks and block size and applies to regular files. Formatting either count successfully does not make it the other quantity.

Name the model field precisely, such as logicalBytes or downloadedBytes, and record how it is obtained. If the value is estimated, unknown, or stale, expose that state separately rather than pretending a formatted zero means “not measured.” A negative signed count can represent a delta in one interface and invalid file size in another. Validate it according to the domain before display.

Use the original integer for ordering, thresholds, totals, and persistence. Store a stable unit contract with exported machine-readable data. A display label is not an exact, locale-independent serialization format, even when it looks like an ordinary number followed by letters.

Binary and decimal styles answer different questions

Apple documents .binary as showing 1024 bytes as one KB and .decimal as showing 1000 bytes as one KB. These scale choices are explicit. The .file and .memory styles express platform-specific presentation conventions rather than a promise that every operating-system release will choose one immutable scaling rule.

Choose a style deliberately when comparing measurements across systems. For a native file-size interface, a platform convention can be appropriate. For a tool that explains a binary block count, explicit binary scaling may be clearer. Neither choice changes the measured bytes, and a comparison should never subtract rounded labels to calculate the difference.

Do not infer IEC-standard suffix spelling from the word “binary.” Apple’s description itself uses KB for the 1024-byte example. If a specification requires a precise KiB/MiB representation, define and verify that output requirement rather than assuming every localized formatter uses those suffixes.

Configure the output policy explicitly

The following Swift fixture restricts the output to kilobytes and suppresses unit labels so it can compare the selected scale on the current macOS environment. It avoids asserting universal English strings or punctuation.

import Foundation

let bytes: Int64 = 1_048_576

func display(_ value: Int64, style: ByteCountFormatter.CountStyle) -> String {
    let formatter = ByteCountFormatter()
    formatter.countStyle = style
    formatter.allowedUnits = [.useKB]
    formatter.includesUnit = false
    formatter.isAdaptive = false
    return formatter.string(fromByteCount: value)
}

let binary = display(bytes, style: .binary)
let decimal = display(bytes, style: .decimal)
precondition(!binary.isEmpty && !decimal.isEmpty)
precondition(binary != decimal)
precondition(bytes == 1_048_576)

allowedUnits limits which units the formatter may use. Its default allows platform-appropriate selection. Restricting units can help a column stay comparable, but it also changes visual magnitude and precision. A table showing only megabytes needs a plan for tiny nonzero values that round to a label near zero.

isAdaptive changes precision policy. Apple’s documentation describes the adaptive algorithm as platform specific, with a historical example using different fractional precision at different magnitudes. Do not elevate that historical example into a permanent contract for every supported release.

Use consistent configuration for comparable values in the same interface. If one row allows all units and another is forced into gigabytes, the visual comparison can be misleading despite both strings being individually valid. Sorting remains based on integer bytes, not the displayed text.

Exact-count presentation is still presentation

includesActualByteCount can add the actual byte count parenthetically when the main portion does not already show it exactly. Apple documents that it has no effect if the value or unit is excluded. The setting is useful for inspection, but its output remains localized text.

let detail = ByteCountFormatter()
detail.countStyle = .binary
detail.allowedUnits = [.useMB]
detail.includesActualByteCount = true
detail.includesCount = true
detail.includesUnit = true
let inspectionLabel = detail.string(fromByteCount: bytes)
precondition(!inspectionLabel.isEmpty)

let machineRecord: [String: Int64] = ["logicalBytes": bytes]
precondition(machineRecord["logicalBytes"] == 1_048_576)

The record retains exact data independently of the label. If a support export needs both, export two named fields with different purposes. Avoid parsing a parenthetical phrase back into a number: localized grouping, wording, and unit presentation are not a durable interchange grammar.

An exact count also does not certify the underlying measurement. It can be exact for a stale snapshot or the wrong quantity. Include timestamp or revision information when a changing file or transfer needs that context, and make refresh behavior visible rather than modifying cached labels without updating their source values.

Preserve arithmetic before formatting

Add integers before displaying a total. Adding individually rounded values can produce a total inconsistent with the underlying data. The discrepancy may be small in a UI and unacceptable in accounting or capacity decisions.

Check overflow when aggregating signed 64-bit counts. Swift’s reporting arithmetic can make that failure explicit rather than silently losing information:

let first: Int64 = 900_000
let second: Int64 = 900_000
let sum = first.addingReportingOverflow(second)
precondition(!sum.overflow)
let totalLabel = display(sum.partialValue, style: .decimal)
precondition(!totalLabel.isEmpty)

A real application should return a normal error or use an appropriate wider representation if its totals exceed the model’s range. The fixture’s precondition is a test assertion, not a recovery strategy for user-controlled input. Do not convert large exact counts to floating-point merely to simplify formatting without reviewing precision loss.

Treat localization as part of the UI contract

Localized labels can vary in separators, spacing, words, and length. Test layout with representative user environments instead of allocating a width based on one short English example. Keep the formatter’s result intact rather than splitting on a presumed ASCII space to extract a value and unit.

Accessibility should describe the measurement meaning alongside the formatted value. A screen reader hearing “size” may need to know whether it is download size or size on disk. The formatter cannot supply the missing application context.

Apple’s current overview recommends Swift byte-count format-style APIs as a declarative alternative and explains that Foundation caches identical format-style instances. That is a useful direction for new Swift interfaces. This article uses ByteCountFormatter to make its established configuration boundary explicit; adopting a newer style API still requires the same measurement, scaling, and exact-data decisions.

Check deployment availability before adopting those newer APIs: ByteCountFormatStyle requires macOS 12, and Measurement.FormatStyle.ByteCount requires macOS 13. Keep ByteCountFormatter or another supported approach when an application targets earlier releases.

Test boundaries without inventing universal strings

Test zero, a single byte, values near decimal and binary thresholds, the largest expected production count, and negative deltas if the feature supports them. Exercise restricted units, adaptive precision, actual-count display, and configurations that omit the count or unit. Assert domain invariants separately from localized snapshots.

For golden UI strings, record the operating-system version and user-language environment. Keep snapshots scoped to that environment, and distinguish legitimate presentation changes from measurement regressions. A label changing after an operating-system update is not automatically data corruption; a changed integer or wrong scale selection is a different issue.

The Swift examples above were executed with Foundation on macOS. They verify explicit configuration, differing scale output, preserved integers, and checked aggregation in that environment. They do not claim identical punctuation across every language or release. A production formatter is trustworthy when display policy can evolve without changing the exact data or the decisions based on it.

Related:

Sources:

Comments