NumberFormatter 货币格式化属性详解

0 阅读3分钟

深入理解代替单纯记忆

前面分享了iOS IAP 本地货币展示:从一个需求到搞清楚 priceLocaleNumberFormatter部分比较重要,单独开一篇进行记录

一、一句话总模型

numberStyle 决定走哪条格式管道currencyCode 决定是什么钱(货币身份)currencySymbol 决定符号长什么样(仅 .currency 管道)internationalCurrencySymbol 决定 ISO 管道显示什么标识


二、numberStyle:管道选择器

numberStyle 是最关键的属性,决定底层走哪条 ICU 格式化路径。

.currency

面向用户 UI 的本地化货币格式,输出「货币符号 + 数字」组合。货币符号由 locale 推导(如 en_US$),currencyCode 可显式覆盖 locale 推导的货币身份,currencySymbol 可进一步覆盖最终显示的符号字符串;符号位置、小数位、分组分隔符均由 locale 决定。

locale输出示例说明
en_US$1,000.00符号在左,点号小数
de_DE1.000,00 €符号在右,逗号小数
ja_JP¥1,000无小数位(JPY 规范)
ar_SAر.س. 1,000.00符号在左,阿拉伯数字格式

适用场景:面向用户的价格展示(应用内购、充值、价格标签等)。


.currencyISOCode

为什么需要它? .currency 使用的货币符号(如 $)存在天然歧义——$ 同时代表美元、加元、澳元、港元等数十种货币,在跨区域的日志、数据上报或金融报表场景中,仅凭符号无法准确识别货币种类。.currencyISOCode 改用 ISO 4217 标准代码(如 USDCADHKD),全球唯一、无歧义。

输出「ISO 4217 货币代码 + 数字」。代码由 locale 推导(与 .currency 一致),currencyCode 可显式覆盖 locale 推导值,internationalCurrencySymbol 可进一步覆盖最终显示的字符串。

locale输出示例说明
en_USUSD 1,000.00代码在左
de_DE1.000,00 EUR代码在右(跟随 locale 习惯)
ja_JPJPY 1,000无小数位

适用场景:金融报表、服务端日志、数据上报等需要「无歧义货币标识」的场合。


.currencyPlural

输出「数字 + 货币全称(自然语言)」,遵循目标语言的单复数规则。

locale + 数值输出示例说明
en_US, 1.001.00 US dollar单数
en_US, 2.002.00 US dollars复数
zh_CN, 1.001.00人民币中文无单复数变化

适用场景:需要「读出来」的场景(如语音播报、无障碍文本)。日常 UI 展示几乎不使用。


三、核心属性说明

currencyCode:货币身份(数据源)

决定「这是哪种钱」,是数据源,不是显示配置。

  • 确定货币单位(USD / JPY / EUR 等)
  • 决定默认 currencySymbol$ / ¥ /
  • 决定货币专属小数位数(JPY=0,KWD=3,USD=2)
  • 未显式设置时从 locale 自动推导

currencySymbol:符号展示(UI 层 override)

只在 .currency 管道生效,属于视觉层覆盖,不影响货币语义。

  • .currency → 生效;未设置时由 locale 自动推导
  • .currencyISOCode → 无效,被忽略

覆盖示例:

let f = NumberFormatter()
f.numberStyle = .currency
f.locale = Locale(identifier: "en_US")
// 默认:$1,000.00

f.currencySymbol = "US$"
// 覆盖后:US$1,000.00(常用于区分美元与其他 $ 货币)

f.currencySymbol = "💰"
// 覆盖后:💰1,000.00

internationalCurrencySymbol:ISO 管道专用标识

只在 .currencyISOCode 管道生效,属于标识层覆盖,不走 symbol 体系。

  • .currencyISOCode → 生效;未设置时直接使用 currencyCode 的值(如 USD
  • .currency → 无效,被忽略

覆盖示例:

let f = NumberFormatter()
f.numberStyle = .currencyISOCode
f.locale = Locale(identifier: "en_US")
f.currencyCode = "USD"
// 默认:USD 1,000.00

f.internationalCurrencySymbol = "美元"
// 覆盖后:美元 1,000.00

四、三条管道关系图

flowchart TD

A[numberStyle] -->|.currency| B[Currency Pipeline]
A -->|.currencyISOCode| C[ISO Pipeline]
A -->|.currencyPlural| D[Plural Pipeline]

%% =======================
%% currency pipeline
%% =======================
B --> E[currencyCode]
E -->|if not set| E1[locale 推导 currencyCode]

B --> F[currencySymbol - local symbol]
F --> G[ICU number formatting]

%% =======================
%% ISO pipeline
%% =======================
C --> H[currencyCode]
H -->|if not set| H1[locale 推导 currencyCode]

C --> I[internationalCurrencySymbol]
I --> G

%% =======================
%% plural pipeline
%% =======================
D --> J[currencyCode]
J -->|if not set| J1[locale 推导 currencyCode]

D --> K[locale plural rules]
K --> G

%% =======================
%% shared engine
%% =======================
G --> L[locale number formatting]
L --> M[final output string]

五、验证代码与输出结果

func printTest(_ label: String, _ f: NumberFormatter) {
    print("---- (label) ----")
    print(f.string(from: 1000) ?? "nil")
}

// 测试 1:默认 currency
let f1 = NumberFormatter()
f1.numberStyle = .currency
f1.locale = Locale(identifier: "en_US")
f1.currencyCode = "USD"  // locale 已能推导出 USD,此行与不设置效果相同
printTest("Default currency", f1)
// $1,000.00

// 测试 2:覆盖 currencySymbol
let f2 = NumberFormatter()
f2.numberStyle = .currency
f2.locale = Locale(identifier: "en_US")
f2.currencyCode = "USD"  // 同上,此行与不设置效果相同
f2.currencySymbol = "💰"
printTest("Override currencySymbol", f2)
// 💰1,000.00

// 测试 3:ISO style + 覆盖 internationalCurrencySymbol
let f3 = NumberFormatter()
f3.numberStyle = .currencyISOCode
f3.locale = Locale(identifier: "en_US")
f3.currencyCode = "USD"
f3.currencySymbol = "💰"  // 被忽略
f3.internationalCurrencySymbol = "US_DOLLAR"
printTest("ISO style + override symbols", f3)
// US_DOLLAR 1,000.00

结论:

  • 测试 2 验证 currencySymbol.currency 管道生效
  • 测试 3 验证 currencySymbol.currencyISOCode 管道被忽略,internationalCurrencySymbol 才生效

六、工程实践结论

多区域内购场景的正确姿势

let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = priceLocale   // 来自 SKProduct.priceLocale

为什么用 .currency 而不是 .currencyISOCode:

内购价格展示的受众是用户,用户习惯看货币符号($¥),ISO 代码(USDEURJPY)是机器/金融领域的表达方式,放在 UI 上不够直观。.currencyISOCode 适合日志、数据上报等对歧义敏感、不面向用户的场合。

只设置这两个属性,原因:

  1. SKProduct.priceLocale 已经将 localecurrencyCode 对齐(Apple 保证)
  2. currencyCodelocale 自动推导,无需手动设置
  3. 手动设置 currencyCode 但不同步 locale,会导致「格式规则与货币符号不匹配」——例如 locale = ar_SAcurrencyCode = USD,输出既非标准美元也非标准里亚尔
  4. currencySymbol 同理,修改后破坏 locale 一致性,不适合多区域场景