Sprinter 是一个用于 Mac 和 iOS 的库,它通过使用 printf/NSLog 格式标记约定在运行时格式化字符串。
目标是提供一个与 printf 规范完全兼容并为 Swift 提供友好接口的类型安全字符串格式化接口,同时兼容 Apple 的用于处理 Objective-C 数据类型的专有扩展。
"Sprinter" 这个名字来源于 "String-Printer",就像是 C 标准库中的 sprintf
函数。
虽然 Swift 已经以 String(format:arguments:)
初始化器形式提供了字符串格式化支持,但 Swift 的支持只是对 Objective-C API 的一种比较粗糙的封装,并且缺少一些标准的 printf 格式化功能和数据类型支持。例如,在 Swift 中无法使用以下格式字符串:
"Hello %s, how are you?"
因为 %s
标记期望一个 C 字符串(一个指向以空字符结束的 CChar
数组的指针),而 Swift 的 String(format:arguments:)
方法不会接受它。相反,您必须使用平台特定的 %@
标记,这限制了字符串在不同平台之间的重用性。
Swift 也无法提供验证或检查格式字符串的方法。如果格式包含错误,或者在代码中的格式参数与代码中的参数不匹配,则字符串将在运行时显示错误,或者更糟,可能会导致崩溃或静默内存损坏。
Sprinter 通过公开每个格式字符串的参数类型来解决这些问题,因此您可以编写运行时验证逻辑并优雅地处理错误。
Sprinter 库也可以用作基于字符串构建时验证的单元测试的基础,甚至是代码生成管道的一部分,以提供强类型字符串属性和方法。
Sprinter 基于原始的 IEEE printf spec 以及 Apple 的Objective-C扩展 实现了一个健壮的字符串格式解析器。它利用 Swift 的字符串格式器进行内部处理,但执行预验证和参数类型转换,以确保永远不会传递无效类型到底层实现。
Sprinter 包含一个综合的测试套件以确保符合规范,并保证与 Apple 格式器的输出兼容。
Sprinter API 整个封装在一个文件中,所有公开的都带有前缀或命名空间,因此您只需将 Sprinter.swift
文件拖入您的项目,即可使用它。如果愿意,Mac 和 iOS 上有一个框架可供导入,或者您可以在 Linux 上使用 CocoaPods、Carthage 或 Swift Package Manager。
要使用 CocoaPods 安装 Sprinter,请在 Podfile 中添加以下内容
pod 'Sprinter', '~> 0.2.0'
Sprinter 兼容 Swift 3.2 和 4.x,支持 iOS 9 或 macOS 10.0 及以上版本
要使用 Sprinter 格式化字符串,您首先创建一个 FormatString
实例,如下所示
let formatString = try FormatString("I have %i apples and %i bananas")
注意 try
关键字 - FormatString
构造函数会验证字符串,如果格式无效,则会抛出错误。构造格式字符串对象后,您可以使用 print()
方法输出格式化字符串。该方法支持可变参数,方便传递参数。还有一种形式接受一个参数数组。
您可以使用 print()
方法如下所示
let string = try formatString.print(5, 6)
print(string) // I have 5 apples and 6 bananas
您会注意到 print()
函数也要求 try
。如果传入的参数与原始格式字符串的占位符不匹配,则该方法会抛出错误。由 FormatString
构造函数或 print()
方法抛出的错误都将为 FormatString.Error
类型,例如
let formatString = try FormatString("I have %y apples") // throws FormatString.error.unexpectedToken("y")
let string = try FormatString("I have %i apples").print("foo") // throws FormatString.error.argumentMismatch(1, String.self, Int.self)
在调用 print()
方法之前,您可以通过使用 FormatString
的 types
属性来确定所需的参数类型,该属性返回一个 Swift 类型值数组
let types = formatString.types
print(types) // Int, Int
这通常在运行时没有太大用处(错误的参数将是一个编程错误,应该在发布之前得到修复),但它可以用于自动化测试,以验证给定本地化字符串键在不同语言中具有相同的参数类型。
FormatString
构造函数还接收一个可选的 locale
参数,可用于本地化输出
let french = try FormatString("I have %i apples", locale: Locale(identifier: "fr-FR"))
这将影响特定于区域设置的对齐和标点符号的显示方式,例如
let english = try FormatString("%'g", locale: Locale(identifier: "en-US")
try print(english.print(1234.56)) // 1,234.56
let french = try FormatString("%'g", locale: Locale(identifier: "fr-FR")
try print(french.print(1234.56)) // 1 234,56
let german = try FormatString("%'g", locale: Locale(identifier: "de-DE")
try print(german.print(1234.56)) // 1.234,56
在后台线程上创建 FormatString
实例是安全的。
创建后,给定的 FormatString
实例是无状态的,因此相同的实例可以安全地在多个线程上并发打印字符串。
在打印之前创建 StringFormat
对象可能看起来有些繁琐,但它有两个作用
它允许在使用之前验证和检查字符串的类型。这意味着您可以在调用时确信不会出现意外的错误。
昂贵的字符串解析和 NumberFormatter
初始化步骤只需执行一次,然后存储起来,无需每次字符串显示时都重复执行。
因此,建议您存储并重用您的 FormatString
对象。您可以在一开始做这件事,也可以在串行第一时间每个字符串显示时做这件事 - 无论哪一个对您的应用更有意义。
一种好的方法是创建一个封装器函数,封装您的应用特定的字符串要求。例如,您可能希望在产品中忽略字符串格式错误(因为那时修复得太晚),只显示一个空字符串。以下是在您的应用中可能使用的示例封装器
private var cache = [String: FormatString]()
private let queue = DispatchQueue(label: "com.Sprinter")
func localizedString(_ key: String, _ args: Any...) -> String {
do {
var formatString: FormatString?
queue.sync { formatString = cache[key] }
if formatString == nil {
formatString = try FormatString(NSLocalizedString(key, comment: ""), locale: Locale.current)
queue.async { cache[key] = formatString }
}
return try formatString?.print(arguments: args) ?? ""
} catch {
// Crash in development, but not in production
assertionFailure("\(error)")
return ""
}
}
此函数提供
Localizable.strings
文件中的键FormatString
实例缓存,以获得更好的性能这只是示例方法,但对于大多数用例都应有效。