NumberFormatter 货币格式化属性详解

深入理解代替单纯记忆

前面分享了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_DE 1.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_US USD 1,000.00 代码在左
de_DE 1.000,00 EUR 代码在右(跟随 locale 习惯)
ja_JP JPY 1,000 无小数位

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


.currencyPlural

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

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

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


三、核心属性说明

currencyCode:货币身份(数据源)

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

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

currencySymbol:符号展示(UI 层 override)

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

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

覆盖示例:

ini 复制代码
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 → 无效,被忽略

覆盖示例:

ini 复制代码
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]

五、验证代码与输出结果

ini 复制代码
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 才生效

六、工程实践结论

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

ini 复制代码
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 一致性,不适合多区域场景
相关推荐
终端安全笔记19 小时前
iOS 27 之后「策略空转」:设备升级不报错,但旧策略不再管它
android·网络·安全·ios·智能手机
9765033351 天前
iOS 上架 4.3a 被拒【uniapp专讲】
flutter·ios·objective-c·uniapp·swift
黑化旺仔1 天前
【OC】KVO
macos·ios·objective-c·cocoa
HouWan2 天前
SwiftUI 小技巧:如何读取 SwiftUI Font 的详细信息
ios·swiftui·apple
李游Leo2 天前
HarmonyOS 7 实战开发 05:把页面做到可上线状态
ios·harmonyos
终端安全笔记2 天前
描述文件、企业证书、MDM 不是三个名字,是三层
android·ios·智能手机
传奇开心果编程2 天前
【SwiftUI入门练中学】第1课 从零开始
学习·ui·ios·swiftui·swift
丿Wayne2 天前
iOS Token 刷新实战(Actor 避免并发重复刷新)
ios·swift·token·actor·swift并发
终端安全笔记3 天前
iOS 27 给了租赁一个新工具,但它只认受监督的设备
android·网络·安全·ios
大熊猫侯佩3 天前
独立 App 首发完成 iPhone Duo 展开态适配
ios·swiftui·swift