Lift 是一个 Swift 库,用于生成值并将值提取到类似 JSON 的数据结构中。 Lift 的设计充分考虑了以下要求
Jar
容器使用值语义。Lift 简单而强大。 让我们看看如何将其与自定义类型一起使用
struct User {
let name: String
let age: Int
}
只需遵守 JarElement
协议,即可让 Lift 知道如何转换您的类型
extension User: JarElement {
init(jar: Jar) throws {
name = try jar["name"]^
age = try jar["age"]^
}
var jar: Jar {
return ["name": name, "age": age]
}
}
然后,给定一些 JSON,您现在可以构造一个 Jar
并使用 lift 运算符 ^
从中提取用户
let json = "[{\"name\": \"Adam\", \"age\": 25}, {\"name\": \"Eve\", \"age\": 20}]"
let jar = try Jar(json: json)
var users: [User] = try jar^
将您的模型值移回 JSON 也同样容易
users.append(User(name: "Junior", age: 2))
let newJson = try String(json: Jar(users), prettyPrinted: true)
Lift 甚至可以与其他类似 JSON 的结构化数据一起使用,例如 p-list 和 UserDefaults
let users: [User] = try UserDefaults.standard["users"]^
查看用法部分,了解更多信息和示例。
9.3+
9.0+
10.11+
9.0+
2.0+
github "iZettle/Lift" >= 2.3
platform :ios, '9.0'
use_frameworks!
target 'Your App Target' do
pod 'Lift', '~> 2.3'
end
import PackageDescription
let package = Package(
name: "Your Package Name",
dependencies: [
.Package(url: "https://github.com/iZettle/Lift.git",
majorVersion: 2)
]
)
Swift 4 引入了 Codable
,并“承诺”一劳永逸地解决与 JSON 的交互。 是的,显示的许多示例都非常神奇。 但是,当您的模型开始从简单模型转变为模型和 JSON 之间的映射时,这种魔力似乎就消失了。 现在您又回到自己实现一切,并且使用的是相当冗长的 API。 当前版本的 Swift 还缺少用于动态构建和解析 JSON 的 API(不经过模型对象),这在例如构建和解析网络请求时很常见。 因此,我们相信第三方 JSON 库的需求在未来一段时间内仍然存在。
让我们从一个简单的示例开始,了解如何从一些键值结构化数据(例如 JSON)中提取数据
let jar: Jar = ["name": "Lift", "version": 1.0]
let name: String = try jar["name"]^
let version: Double = try jar["version"]^
Jar
是 Lift 的异构值容器。 在此示例中,它保存一个字典。 运算符 ^
(称为 lift 运算符)用于从 jar 容器中提取值。 因为 jar 通常保存编译时未知的值,所以提取它们可能会失败。 如果值缺失、值类型与预期不符或某些其他验证失败,则可能会发生这种情况。 这就是为什么您总是会在 lift 运算符 ^
存在时看到 try
。
如前所述,JSON 并不总是以字典(键值)的形式出现,也可以是简单的原始类型或其他 JSON 对象的数组
let i: Int = try Jar(1)^
let b: Bool = try Jar(true)^
let a: [Int] = try Jar([1, 2, 3])^
let jar: Jar = ["value": "lift"]
let s: String = try jar["value"]^
^
运算符被重载,以允许一致的类型提取为类型本身或其可选版本。 您还可以提取一致类型的数组或可选数组
let i: Int = try jar^
let i: Int? = try jar^
let i: [Int] = try jar^
let i: [Int]? = try jar^
Jar
实现了键和索引的下标操作,并允许它们嵌套
let date: Date = try jar["payments"][3]["date"]^
jar["payments"][2]["date"] = Date()
Lift 添加了便捷的初始化程序,用于从 JSON 构造 Jar
和将其返回
let json = "{ \"val\": 3, \"vals\": [ 1, 2 ] }"
let jar = try Jar(json: json)
let jsonString = try String(json: jar, prettyPrinted: true)
let jsonData = try Data(json: jar, prettyPrinted: false)
您也可以自己处理序列化,只需传递一个 Any
值
let json: Any = ...
let jar = try Jar(checked: json) // Will validate when constructed - slower
let jar = Jar(unchecked: json) // Will lazily validate at access - faster
let any: Any = try jar.asAny()
为了帮助创建 JSON,Jar
实现了几个按字面量可表达的协议,因此您可以编写如下代码
func send(_ jar: Jar) { ... }
send(true)
send(1)
send(3.14)
send("Hello")
send(["val": 5])
send([1, 2, 3])
当 Jar
无法被编译器推断时,您可以显式指定类型
let jar: Jar = ["val": 5]
let jar: Jar = [1, 2, 3]
您还可以构建嵌套的层次结构
let jar: Jar = [5, ["val": [1, 2]]]
当然,您也可以从自定义类型构建 JSON
let jar: Jar = ["payment": payment, "date": date]
因为 Jar
是一种具有值语义的值类型,所以您可以修改声明为 var
的 Jar
值。
var jar = Jar()
jar["payment"] = payment
jar["date"] = Date()
var jar = Jar()
jar.append(payment)
jar.append(Date())
如果需要在传递 JSON 之前对其进行修改,只需复制一份
func receive(jar: Jar) {
var jar = jar
jar["timeReceived"] = Date()
// ...
}
Jar
永远不会绑定到特定的类型或值,因此始终可以更改它
var jar: Jar = true // jar holds a boolean
jar = [1, 2] // holds an array
jar["val"] = 1 // holds a dictionary
jar = 4711 // holds an integer
jar = ["val2": 2] // holds a dictionary
jar = "Hello" // holds a string
Lift 支持使用原始类型数组
var jar: Jar = [1, 2, 3]
jar[1] = 4
jar.append(5)
let val: Int = try jar[2]^
以及自定义类型的数组
var jar: Jar = [Payment(...), Payment(...), ...]
let payments: [Payment] = try jar^
jar[2] = Payment(...)
有时需要存在值,有时它是可选的。
let i: Int = try jar["val"]^ // Will throw if val is missing or not an Int
let i: Int? = try jar["val"]^ // Will return nil if val is missing or null, else throw if not an Int
let i: Int = try jar["val"]^ ?? 4711 // Will throw if val is present and is not an Int
为了您的方便,Lift 将设置为 JSON null 的值视为与缺失值相同。 但是,如果您需要检查实际 null 值的存在,您可以编写
if let _: Null = try? jar["val"]^ {
//...
}
在构建 JSON 时,某些值是可选的非常常见
let optional: Int? = nil
var jar: Jar = ["val": 1]
jar["optional"] = optional // -> {"val": 1}
也可以内联添加可选值
var jar: Jar = ["val": 1, "optional": optional] // -> {"val": 1}
如果您实际想要一个 JSON null 值,可以使用常量 null
var jar: Jar = ["val": 1, "optional": optional ?? null] // -> {"val": 1, "optional": null}
有时您的 JSON 的某些部分可能包含不同类型的有效类型的联合。 然后,您可以测试您支持的不同变体
let any: Any? = NSJSONSerialization...
let jar = try Jar(any) // any could be a dictionary, array or a primitive type
if let int: Int = try? jar^ {
// ...
} else if let ints: [Int] = try? jar^ {
// ...
} else if let int: Int = try? jar["value"]^ {
// ...
}
JSON 也支持混合类型的数组
let jar = try Jar(any) // [ 1, [1, 2], { "val" : 3 } ] -- [Int, Array, Dictionary]
let int: Int = try jar[0]^
let array: [Int] = try jar[1]^
let dict: Jar = try jar[2]^
有时您需要在从 Jar
提取值之前对其进行转换。 当您使用无法遵守 JarRepresentable
的类型时,可能会发生这种情况,例如在使用元组时
typealias User = (name: String, age: Int)
let users: [User] = try (jar^ as [Jar]).map { jar in
try (jar["name"]^, jar["age"]^)
}
或者当您的类型不符合 JarRepresentable
时,因为它可能需要一些额外的初始化数据
let account: Account = ...
let payments: [Payment] = try (jar["payments"]^ as Array).map {
Payment(jar: $0, account: account)
}
即使您可以手动转换值以添加额外的初始化数据,但通常更方便的是将此数据添加到 jar 的上下文中。 jar 上下文将在下面进一步描述。
设置和获取未知类型的值并非 JSON 独有。 许多 Cocoa API 使用字典,其中许多基于与 JSON 类似的原则,例如 p-list。 Lift 提供了用于扩展这些类型的协议,以授予它们访问 Lift 功能的权限。 例如,Lift 已经扩展了 UserDefaults
// extension UserDefaults: MutatingValueForKey { }
let userDefaults = UserDefaults.standard
let date: Date? = try userDefaults["lastLaunched"]^
userDefaults["lastLaunched"] = Date()
let payments: [Payments] = try userDefaults["payments"]^ ?? []
开箱即用,Lift 库支持 JSON 字典、数组及其原始类型:字符串、数字、布尔值和 null。 但是,也很容易扩展您自己的类型以与 Lift 库一起使用。
为了能够使用 lift 运算符 ^
从 Jar
中提取值,您需要使您的类型符合 JarConvertible
协议
protocol JarConvertible {
init(jar: Jar) throws
}
为了能够将您的类型转换为 Jar
,您需要使您的类型符合 JarRepresentable
协议
protocol JarRepresentable {
var jar: Jar { get }
}
通常会同时实现这两个协议,因此可以使用方便的 JarElement
类型别名
typealias JarElement = JarConvertible & JarRepresentable
Lift 库包括最常见原始类型(例如 Int
、Bool
、String
等)的扩展,通过使它们符合 JarElement
。
您的自定义类型通常是简单类型,例如
struct Money {
let fractionized: Int
}
extension Money: JarElement {
init(jar: Jar) throws {
fractionized = try jar^
}
var jar: Jar { return Jar(fractionized) }
}
let jar: Jar = ["amount": Money(fractionized: 2000)]
let amount: Money = try jar["amount"]^
或者更常见的是,更复杂和类似记录的类型,例如
struct Payment {
let amount: Money
let date: Date
}
extension Payment: JarElement {
init(jar: Jar) throws {
amount = try jar["amount"]^
date = try jar["date"]^
}
var jar: Jar {
return ["amount": amount, "date": date]
}
}
let jar: Jar = ["payment": Payment(...)]
let payment: Payment = try jar["payment"]^
为了更轻松地使具有原始值的自定义枚举符合要求,Lift 提供了一些默认实现。 您所要做的就是使枚举符合 JarElement
,以便能够将其与 Jar
一起使用
enum MyEnum: String, JarElement {
case one, two, three
}
let jar: Jar = ["enum": MyEnum.two]
let str: String = try jar["enum"]^ // -> "two"
let myEnum: MyEnum = try jar["enum"]^ // -> .two
JarConvertible
要求您实现一个必需的 init。 如果您使用无法更新源本身的非最终类,例如当该类源自 Objective-C 或其他外部源时,这可能会出现问题。 在这些情况下,您必须改用 Liftable
协议
extension MyClass: Liftable {
static func lift(from jar: Jar) throws -> MyClass {
// Implementation
}
}
Lift 库不强制规定自定义类型的结构,也允许追溯建模。 如何决定在类型和 JSON 之间进行映射取决于您。 例如,您可能有带有关联值的枚举(在此示例中是递归的)
// [ { "type": "Product", "uuid": ”3b0bb980-2c…” },
// { "type": "Folder", "name": "Coffee", "items": [
// { "type": "Product", "uuid": ”3e493140-2c…” },
// { "type": "Product", ”uuid": ”3e623780-2c…” }] },
// ... ]
indirect enum FlowLayout {
case product(uuid: UUID)
case folder(name: String, items: [FlowLayout])
}
因为 JSON 格式的类型系统比 Swift 弱,所以更严格的验证变得更加重要
extension FlowLayout: JarConvertible {
init(jar: Jar) throws {
switch try jar["type"]^ as String {
case "Product":
self = try .product(uuid: jar["uuid"]^)
case "Folder":
self = try .folder(name: jar["name"]^, items: jar["items"]^)
case let type:
throw jar.assertionFailure("Unknown layout type: \(type)")
}
}
}
即使对于这些更复杂的类型,JSON 的生成仍然非常简单
extension FlowLayout: JarRepresentable {
var jar: Jar {
switch self {
case let .product(uuid):
return ["type": "Product", "uuid": uuid]
case let .folder(name, items):
return ["type": "Folder", "name": name, "items": items]
}
}
}
因为 JSON 通常是嵌套的,所以使用一些定位和上下文扩展错误会很有用。 Lift 尝试跟踪最接近的上下文和到您的数据的“键路径”,并将这些信息公开在 LiftError
中
struct LiftError: Error {
let message: String
let key: String
let context: String
}
因为上下文和键路径在调试期间确实很有价值,所以重要的是在抛出验证错误时不要丢失这些信息。 因此,Lift 已向 Jar
添加了特殊的断言辅助方法,建议您使用
init(jar: Jar) throws {
// ...
try jar.assert(i > 0, "Must greater than zero")
guard validate(...) else {
throw jar.assertionFailure("Not a business nor a person")
}
url = try jar.assertNotNil(URL(string: jar^), "Invalid URL")
// ...
}
有时您的类型的初始化程序需要访问比 JSON 本身包含的更多数据。 例如,您的 Money
类型可能还需要一个货币,但您的 JSON 不提供该货币,或者提供的货币距离实际金额值很远。 这是您可以将货币传递到 jar 的上下文中的位置
struct Money {
let fractionized: Int
let currency: Currency
}
extension Money: JarElement {
init(jar: Jar) throws {
fractionized = try jar^
currency = try jar.context.get() // will extract the currency
}
var jar: Jar { return Jar(fractionized) }
}
let amount: Money = try jar.union(context: currency)["amount"]^
jar 的上下文也可用于自定义类型的编码和解码。 例如,Date
默认将使用 ISO8601 日期格式,但通过在 jar 的上下文中提供另一个 DateFormatter
,您可以自定义日期格式
extension Date: JarConvertible, JarRepresentableWithContext {
init(jar: Jar) throws {
let formatter: DateFormatter = jar.context.get() ?? .iso8601
self = try jar.assertNotNil(formatter.date(from: jar^), "Date failed to convert using formatter with dateFormat: \(formatter.dateFormat)")
}
func asJar(using context: Jar.Context) -> Jar {
let formatter: DateFormatter = context.get() ?? .iso8601
return Jar(formatter.string(from: self))
}
}
由于 JarRepresentable
不提供任何上下文,因此您将改为遵守 JarRepresentableWithContext
,后者在 asJar
中传递上下文
protocol JarRepresentableWithContext: JarRepresentable {
func asJar(using context: Jar.Context) -> Jar
}
上下文可以从外部设置,也可以作为某些其他类型的编码/解码的一部分设置,例如
struct Payment {
let amount: Money
let date: Date
}
extension Payment: JarElement {
init(jar: Jar) throws {
let jar = jar.union(context: DateFormatter.custom)
amount = try jar["amount"]^ // a currency must be provided in the jar's context
date = try jar["date"]^ // date will format using DateFormatter.custom
}
var jar: Jar {
let jar: Jar = ["amount": amount, "date": date]
return jar.union(context: DateFormatter.custom)
}
}
let payment: Payment = try jar.union(context: currency)["payment"]^
Lift 是在过去几年中开发、演进和经过实地测试的,并且在 iZettle 广受好评的销售点应用程序中被广泛使用,用于与 iZettle 的全方位后端服务进行通信。
您可以在我们的 Slack 工作区与我们协作。 您可以提问、分享想法,或者只是参与正在进行的讨论。 要获得邀请,请发送邮件至 ios-oss@izettle.com
要了解更多关于 Lift 的 API 如何演变的信息,我们建议您阅读以下文章: