WrapModel
WrapModel 是在 Swift(同时保持 Objective-C 兼容性)中将 JSON 数据转换为可用的模型对象的一种不同方法。而不是将 JSON 视为原始输入并立即将其转换为其他东西,WrapModel 将 JSON 数据包裹在一个知道如何访问数据部分的对象中,并且仅在需要时才执行转换。
// With a bit of JSON data like this
let modelStr =
{
"last-name": "Smith",
"first-name": "John",
"most-recent-purchase": "05/02/2018",
"cust-no": 12345
}
// A model is defined like this - Objective C compatible with property wrappers (requires Swift 5.1)
class Customer: WrapModel {
@StrProperty("last-name") var lastName
@StrProperty("first-name") var firstName
@DateProperty("most-recent-purchase", dateType: .mdySlashes) var lastPurchase
@IntProperty("cust-no") var custNumber
}
// It's possible to use WrapModel with Swift 4.2, but you must declare private properties
// and public accessors if you want to retain Objective C compatibility. See the Usage section below.
// Model properties can be read/written just like any other member of a class.
// The model is marked as mutable/immutable on creation.
if let cust = Customer(json: modelStr, mutable: true) {
// Read properties
let fullName = "\(cust.firstName) \(cust.lastName)"
print("customer is \(fullName)")
// Mutate a property
cust.lastPurchase = Date()
}
内容
事由
WrapModel
提供了以JSON形式接收数据模型的结构化访问。模型可以直接使用JSON字符串(或Data)或数据字典来初始化。目前有许多解决方案提供了此类功能,但WrapModel
的创建是为了实现几个具体的目标
- 在 Swift 中声明简单
- 易于使用,使用模式和直接属性相似
- 速度 - 数据转换是懒加载的
- 仅定义一次属性(无需维护第二个列表)
- 易于转换数据类型和枚举
- 灵活的结构
- 易于调试
- 可强制执行的不可变性
- Objective-C 兼容性
以下将详细介绍这些目标,但以下是WrapModel
实现目标的主要方式之一
- 保留原始数据字典
- 在访问时懒惰地转换属性数据
- 缓存已转换(或已更改)的属性以避免多次转换
需求
Swift 4.2+ | iOS 10+
使用基于属性的封装器定义属性需要一个支持Swift 5.1的版本
交流
- 要报告错误或请求功能,请创建一个问题。
- 如果您想贡献更改,请提交一个拉取请求。
为什么不是Codable?
Swift自身已经包含了Codable
,为什么还要编写一个新解决方案?通过遵守协议,Codable
是一种将数据转换为/从模型对象转换为的便捷方式。这在小、定义明确且一致的数据上效果很好,但导致我忽视它的主要缺点是
- 如果某个属性需要自定义解码,您必须手动定义所有键 - 现在,您基本上在两个地方定义属性
- 所有数据转换都在前端发生 - 如果使用该属性,则无论何时都会发生缓慢的转换
- Codable对象是对编码数据结构的相对严格的反映,而我更需要的是更多的结构灵活性
用法
您的模型类继承自WrapModel
。在底层,每个属性都是一个WrapProperty
子类的实例,它提供了类型和转换。提供了许多WrapProperty
子类,代表所有基本数据类型,包括整数、浮点数、布尔值、字符串、枚举、日期、字典、数组和子模型(见下文)。
可以通过继承WrapProperty
并为其提供从/到闭包的转换来定义新的属性类型,以便将数据转换为任何类型。
WrapModel
为它提供的所有属性类型提供了属性包装器,每个都有不可变和可变两种变化。所有这些都具有可选的价值修改器参数,可以在从属性读取或向属性写入时自定义值(如果需要的话)。
// A Customer model definition using property wrappers - for >= Swift 5.1
// Properties are directly readable and writable using property name. E.g. cust.lastName = "Jones"
class Customer: WrapModel {
@MutStrProperty("last-name") var lastName: String
@MutStrProperty("first-name") var firstName: String
@DateProperty("join-date", dateType: .mdySlashes) var joinDate: Date?
@IntProperty("cust-no") var custNumber: Int
}
除了提供的特定类型的包装器之外,还提供了两个通用属性包装器,用于包装您创建的其他WrapProperty
子类。您的属性对象将被作为参数传递到包装器中。这两个包装器让您区分可以读取和写入的属性(@RWProperty
)以及应该只读的属性。它们也将接受可选的价值修改器闭包参数。
// A Customer model definition using property wrappers - for >= Swift 5.1
// Properties are directly readable and writable using property name. E.g. cust.lastName = "Jones"
class Customer: WrapModel {
@RWProperty( CustomClassProperty("last-name-origin")) var lastNameOrigin: MyCustomClass?
}
也可以使用裸露的属性对象,并通过每个(仅从Swift)上的value
属性访问它们。
// Properties are accessible via value member. E.g. cust.lastName.value = "Jones"
class Customer: WrapModel {
let lastName = WPStr("last-name")
let firstName = WPStr("first-name")
let lastPurchase = WPDate("most-recent-purchase", dateType: .mdySlashes)
let custNumber = WPInt("cust-no")
}
如果您无法使用Swift 5.1或更高版本,您仍然可以使用私有定义/公共访问器模式来为属性获取Objective C可访问性。属性本身被声明为私有,并为访问和写入属性值提供公共访问器。由于基于Swift泛型,属性本身对于Objective C是不可用的。
公共访问器执行与Swift 5.1及更高版本中属性包装器为我们执行的工作相同。
// A Customer model definition using private definition/public accessor pattern - Objective C accessible
// Properties are directly readable and writable using property name. E.g. cust.lastName = "Jones".
class Customer: WrapModel {
// Property definitions
private let _lastName = WPStr("last-name")
private let _firstName = WPStr("first-name")
private let _lastPurchase = WPDate("most-recent-purchase", dateType: .mdySlashes)
private let _custNumber = WPInt("cust-no")
// ObjC compatible accessors
var lastName:String { get { return _lastName.value } set { _lastName.value = newValue } }
var firstName:String { get { return _firstName.value } set { _firstName.value = newValue } }
var lastPurchase:Date? { get { return _lastPurchase.value } set { _lastPurchase.value = newValue } }
var custNumber:Int { get { return _custNumber.value } set { _custNumber.value = newValue } }
}
无论您如何定义模型的属性,您都将以相同的方式初始化它。
// If you have a dictionary, you init from that
let custData: [String:Any] = [
"last-name": "Smith",
"first-name": "John",
"cust-no": 12345,
"join-date": "5/22/2019"
]
let cust = Customer(data: custData, mutable: false)
// or if you have JSON as a String, you can init from that
// WrapModel uses native JSONSerialization to convert to a dictionary
let custJSON: String = """
{
"last-name":"Smith",
"first-name":"John",
"cust-no":12345,
"join-date":"5/22/2019"
}
"""
let cust = Customer(json: custJSON, mutable: false)
线程安全
WrapModel对象创建后是线程安全的。读取和写入属性将通过一个锁定机制进行,该机制利用GCD来允许同时读取和阻塞写入。每个模型都有一个锁,代表一个GCD队列,它与其任何子模型共享。
模型属性
键路径
每个属性都通过一个键路径字符串来定义。这允许模型在其数据字典中找到相关的属性数据。虽然这些数据通常位于此模型字典的顶层,但这并非必须。键路径可以指定为一个点分隔的键列表,以更深入地挖掘数据字典。
默认属性值
WrapModel
属性(以及一般的Swift属性)的类型可以是可选的或非可选的。当一个属性的类型是非可选时,需要提供一个默认值,以防模型的数据字典在该键路径下没有值。您可以在属性的初始化器中指定属性默认值,但提供了一些合理的默认值。例如,非可选整数的默认值是0,非可选集合类型的默认值是空数组/字典。可选类型的默认值是nil。
示例
@IntProperty("return-limit", defaultValue: 12) var returnLimit: Int // specified default
@IntProperty("min-purch-num") var minPurchases: Int // default value is zero
@OptDictProperty("statistics") var stats:[String:Any]? // default value is nil
提供的属性类型
几乎所有的提供的属性类型都有类型别名的简短名称,它与更长的名称对应。下面列出了两种。
请注意,Int和Float需要特殊处理。非整数的类型转换(如1.1)会返回nil。另外,由于浮点数的精度问题,浮点数值通常不能成功转换为Float,因此值必须先转换为Double,然后下转换为Float。对于Int值,非整数会被四舍五入。
注意 - Int?与Objective C不兼容。
简称 | 数据类型 | 长名 | 默认值 | 属性包装器 |
---|---|---|---|---|
WPInt |
Int | WrapPropertyInt | 0 | [Mut]IntProperty |
WPOptInt |
Int? | WrapPropertyOptInt | nil | [Mut]OptIntProperty |
WPFloat |
Float | WrapPropertyFloat | 0.0 | [Mut]FloatProperty |
WPDouble |
Double | WrapPropertyDouble | 0.0 | [Mut]DoubleProperty |
WPBool |
Bool | WrapPropertyBool | false | [Mut]BoolProperty |
简称 | 数据类型 | 长名 | 默认值 | 属性包装器 |
---|---|---|---|---|
WPNumInt |
NSNumber? | WrapPropertyNSNumberInt | nil | [Mut]NumIntProperty |
WPNumFloat |
NSNumber? | WrapPropertyNSNumberFloat | nil | [Mut]NumFloatProperty |
输入可以是数字或字符串;输出总是字符串。注意 - Int?与Objective C不兼容。
简称 | 数据类型 | 长名 | 默认值 | 属性包装器 |
---|---|---|---|---|
WPIntStr |
Int | WrapPropertyIntFromString | 0 | [Mut]IntStrProperty |
WPOptIntStr |
Int? | WrapPropertyOptionalIntFromString | nil | [Mut]OptIntStrProperty |
简称 | 数据类型 | 长名 | 默认值 | 属性包装器 |
---|---|---|---|---|
WPDict |
[String:Any] | WrapPropertyDict | [:] | [Mut]DictProperty |
WPOptDict |
[String:Any]? (可选) | WrapPropertyOptional<[String:Any]> | nil | [Mut]OptDictProperty |
简称 | 数据类型 | 长名 | 默认值 | 属性包装器 |
---|---|---|---|---|
WPStr |
String | WrapPropertyString | "" | [Mut]StrProperty |
WPOptStr |
String? (可选) | WrapPropertyOptional |
nil | [Mut]OptStrProperty |
在 JSON 中,枚举应表示为字符串值。请提供符合 WrapConvertibleEnum
协议的枚举作为模板参数。
简称 | 数据类型 | 长名 | 默认值 | 属性包装器 |
---|---|---|---|---|
WPEnum |
T | WrapPropertyConvertibleEnum | 指定的默认值或未知枚举 | [Mut]EnumProperty / [Mut]EnumUnkProperty |
WPOptEnum |
T | WrapPropertyConvertibleOptionalEnum | nil | [Mut]OptEnumProperty |
对于 Wrapmodel
子类类型,可以是单独的或作为集合的元素。
简称 | 数据类型 | 长名 | 默认值 | 属性包装器 |
---|---|---|---|---|
WPModel |
T? (可选) | WrapPropertyModel | nil | [Mut]ModelProperty |
WPModelDict |
[String:T] | WrapPropertyDictionaryOfModel | [:] | [Mut]ModelDictProperty |
WPOptModelDict |
[String:T]? (可选) | WrapPropertyOptionalDictionaryOfModel | nil | [Mut]OptModelDictProperty |
简称 | 数据类型 | 长名 | 默认值 | 属性包装器 |
---|---|---|---|---|
WPModelArray |
[T] | WrapPropertyArrayOfModel | [] | [Mut]ModelArrayProperty |
WPOptModelArray |
[T]? (可选) | WrapPropertyOptionalArrayOfModel | nil | [Mut]OptModelArrayProperty |
WPEmbModelArray |
[T] | WrapPropertyArrayOfEmbeddedModel | [] | [Mut]EmbModelArrayProperty |
WPOptEmbModelArray |
[T]? (可选) | WrapPropertyOptionalArrayOfEmbeddedModel | nil | [Mut]OptEmbModelArrayProperty |
简称 | 数据类型 | 长名 | 默认值 | 属性包装器 |
---|---|---|---|---|
WPGroup |
T | WrapPropertyGroup | T (非可选) | GroupProperty |
简称 | 数据类型 | 长名 | 默认值 | 属性包装器 |
---|---|---|---|---|
WPDate |
Date? (可选) | WrapPropertyDate | nil | [Mut]DateProperty |
WPDateFmt |
Date? (可选) | WrapPropertyDateFormatted | nil | [Mut]DateFmtProperty |
WPDate8601 |
Date? (可选) | WrapPropertyDateISO8601 | nil | [Mut]Date8601Property |
请注意,整数和浮点数数组需要特殊处理。简单地将包含非整数值的数组(如 1.1)重铸将为 nil。此外,当需要浮点数数组时,由于浮点数的精度可能导致值只能被 Double 包含,因此必须首先将值重铸为 Double,然后再下转成 Float。
简称 | 数据类型 | 长名 | 默认值 | 属性包装器 |
---|---|---|---|---|
WPIntArray |
[Int] | WrapPropertyIntArray | [] | [Mut]IntArrayProperty |
WPFloatArray |
[Float] | WrapPropertyFloatArray | [] | [Mut]FloatArrayProperty |
WPDoubleArray |
[Double] | WrapPropertyArray |
[] | [Mut]DoubleArrayProperty |
WPStrArray |
[String] | WrapPropertyArray |
[] | [Mut]StrArrayProperty |
WPDictArray |
[[String:Any]] | WrapPropertyArray<[String:Any]> | [] | [Mut]DictArrayProperty |
WPOptIntArray |
[Int]? (可选) | WrapPropertyOptionalIntArray | nil | [Mut]OptIntArrayProperty |
WPOptFloatArray |
[Float]? (可选) | WrapPropertyOptionalFloatArray | nil | [Mut]OptFloatArrayProperty |
WPOptDoubleArray |
[Double]? (可选) | WrapPropertyOptionalArray |
nil | [Mut]OptDoubleArrayProperty |
WPOptStrArray |
[String]? (可选) | WrapPropertyOptionalArray |
nil | [Mut]OptStrArrayProperty |
WPOptDictArray |
[[String:Any]]? (可选) | WrapPropertyOptionalArray<[String:Any]> | nil | [Mut]OptDictArrayProperty |
您可以将属性声明为任何指定类型的数组。
WrapPropertyArray<T>
WrapPropertyOptionalArray<T>
关于一些属性类型的更多信息
枚举属性
如果你有一个表示枚举类型的属性,《WrapModel》提供了一个属性类型WrapPropertyEnum
(类型别名WPEnum
),它会为你处理枚举值的转换,前提是你的枚举有一个RawValue
类型为Int
。
- 遵守《WrapConvertibleEnum》协议
- 《WrapConvertibleEnum》协议的符合要求是枚举必须实现一个返回形式为
[String:Enum]
的 dictionary 的conversionDict
函数,其中Enum
是属性的枚举类型。
有三个不同的属性包装器用于遵守《WrapConvertibleEnum》协议的枚举属性。
对于遵守《WrapConvertibleEnum》协议的枚举属性,有三种不同的属性包装器。
[Mut]EnumProperty
是必须的,并返回指定的defaultEnum
枚举值。如果没有显式设置其他值,默认值将被导出到字典或 JSON 中。[Mut]EnumUnkProperty
是必须的,当模型不包含值时,返回指定的unknown
枚举值,但该unknown
枚举值永远不会写入模型,即使显式设置也不会。除非原始模型中存在unknown
值,否则不应将其导出到字典或 JSON 中。[Mut]OptEnumProperty
是可选的,当模型中没有值时返回 nil。
日期属性
WrapPropertyDate
(WPDate
)处理通过枚举指定的几个常见的日期格式。从字符串到传入的翻译尝试首先从指定的日期类型解码,然后还尝试所有它知道的其它类型。转换回字符串始终使用指定的日期类型。这是最宽容的日期属性类型。
当前由 WPDate
支持的日期类型是
dibs // 2017-02-05T17:03:13.000-03:00
secondary // Tue Jun 3 2008 11:05:30 GMT
iso8601 // 2016-11-01T21:14:33Z
yyyymmddSlashes // 2018/02/15
yyyymmddDashes // 2018-02-15
yyyymmdd // 20180215
mdySlashes // 05/06/2018
mdyDashes // 05-06-2018
dmySlashes // 30/02/2017
dmyDashes // 30-02-2017
WrapPropertyDateFormatted
(WPDateFmt
)使用一个 DateFormatter
格式字符串初始化,因此它可以用一致的方式来处理几乎任何格式化为字符串的日期,但这也使其相当灵活,因为日期字符串必须紧密匹配日期格式字符串。
WPDate8601
使用 ISO8601DateFormatter.Options
标志初始化。与 ISO8601DateFormatter
一起使用的选项涵盖了在处理 ISO 8601 格式的日期字符串时使用的几乎所有变化。
嵌入式模型数组
在某些情况下,数组中的子模型可能被封装在一个或多个子字典中,这些子字典的唯一用途就是封装子模型。这通常发生在使用 GraphQL 的情况下,其中模型可以封装在一个像这样的 "node" 字典中
{
"customers": [
{
"node": {
"first-name": "Harry",
"last-name": "Jones",
"cust-no": 123
}
},
{
"node": {
"first-name": "George",
"last-name": "Black",
"cust-no": 456
}
}
]
}
使用 WPEmbModelArray
或 WPOptEmbModelArray
,你可以避免为这些裸封装创建模型类,而是创建一个属性,该属性简单返回模型本身的一个数组。只需为属性初始化器中的 embedPath
参数指定一个点分隔的键路径,属性对象将自动从一层或多层封装中提取模型。
客户的数组属性声明如下
@EmbModelArrayProperty("customers", embedPath:"node") var customers:[Customer]
属性组
属性组定义为子模型,但不深入数据字典的下一级。这有两个原因
-
如果你有一个具有大量属性(其中许多只有在特定情况下才使用)的模型,那么该模型正在创建大量可能不会使用的属性对象。即使它们位于数据字典的顶部,也可以将这些属性分组并在子模型中定义。
-
如果你有一个 "扁平化" 的数据模型,具有属性的逻辑子组,将它们定义为组可以使访问更加合理。
例如,对于一个看起来像这样的扁平数据字典
{
"id": "9107882",
"profile-firstName": "Sandy",
"profile-lastName": "Smith",
"profile-email": "[email protected]",
"profile-company": "Sandy's Stuff",
"contact-firstName": "David",
"contact-lastName": "Smith",
"contact-email": "[email protected]",
"pref-currency": "USD",
"pref-measurementUnit": "IN",
}
你可以创建一个反映更逻辑层次结构的模型
class CustomerModel: WrapModel {
class Profile: WrapModel {
@OptStrProperty("profile-firstName") var firstName:String?
@OptStrProperty("profile-lastName") var lastName:String?
@OptStrProperty("profile-email") var email:String?
@OptStrProperty("profile-company") var company:String?
}
class Contact: WrapModel {
@OptStrProperty("contact-firstName") var firstName:String?
@OptStrProperty("contact-lastName") var lastName:String?
@OptStrProperty("contact-email") var email:String?
}
class Prefs: WrapModel {
@MutEnumProperty("pref-currency", defaultEnum: .usd) var currency:CurrencyEnum
@MutEnumProperty("pref-measurementUnit", defaultEnum: .inches) var measurementUnit:MeasurementUnitEnum
}
@OptStrProperty("id") var id:String?
@GroupProperty() var profile:Profile
@GroupProperty() var contact:Contact
@GroupProperty() var pref:Prefs
}
现在,访问模型看起来更自然,并且在组内的属性对象实际上只有在访问组中的属性时才会创建。
var cust: CustomerModel
print("Contact is: \(cust.contact.firstName)")
print("Company is: \(cust.profile.company)")
cust.pref.measurementUnit = .cm
属性序列化
WrapProperty 拥有 serializeForOutput
成员,该成员确定它在序列化到 JSON 时是否应被发出。默认情况下,这设置为 true,但你可以在属性的声明/初始化中指定 false,以防止属性在序列化输出时被发出。
自定义属性
您可以为所需的任何属性类型创建 WrapProperty
的新子类。您只需要提供转换闭包即可。
// If you want a property of this type based on a data string:
class MyDataType {
let myStringValue:String
init(strData:String) {
myStringValue = strData
}
}
// Create a WrapProperty subclass that transforms back and forth between String and MyDataType
class MyDataTypeProperty: WrapProperty<MyDataType?> {
init(_ keyPath: String) {
super.init(keyPath, defaultValue: nil, serializeForOutput: true)
self.toModelConverter = { (jsonValue:Any) -> MyDataType? in
// Convert dictionary/JSON data to custom property type
guard let str = jsonValue as? String else { return nil }
return MyDataType(strData: str)
}
self.fromModelConverter = { (nativeValue:MyDataType?) -> Any? in {
// Convert property data to format that goes in dictionary/JSON
return nativeValue?.myStringValue
}
}
}
// Then use it in a model class:
class MyModel: WrapModel {
@ROProperty( MyDataTypeProperty("dataString")) var dataProperty:MyDataType?
}
属性包装器 & 值修饰器
泛型属性包装器
使用属性包装器声明属性需要 Swift 5.1 或更高版本。
提供了两个泛型属性包装器 ROProperty
和 RWProperty
,可以用来包装任何 WrapProperty 类型。将 WrapProperty 实例作为参数传递给属性包装器,如下所示:
class MyModel: WrapModel {
@ROProperty( MyDataTypeProperty("dataString")) var dataProperty:MyDataType?
}
ROProperty
用于不可变(只读)属性,并提供无设置器。 RWProperty
允许修改属性(假设模型本身是可变的)。
特定类型属性包装器
还提供了库中包含的大部分 WrapProperty 子类的属性包装器。每个属性包装器的参数可能会有所不同;例如,可以为 StrProperty
指定 defaultValue
参数。对于泛型属性包装器,例如 ModelProperty<T>
,编译器可以从声明中剩余部分推导出必需的类型,因此不需要在尖括号内放置类型。特定类型的属性包装器不需要传递 WrapProperty 实例,它们是在包装器内部创建的。
@StrProperty("stringPath") var someString: String
@ModelProperty("modelPath") var submodel: ModelClass // no need to put <ModelClass> after @ModelProperty
所有提供的属性包装器都有不可变和可变版本。它们的命名习惯是在可变版本的名称开头包含 Mut
。例如,StrProperty
是不可变的,而 MutStrProperty
允许修改属性值(可变)。
值修饰符参数
每个提供的属性包装器,从更通用的ROProperty
和RWProperty
,到更具体的如StrProperty
和IntProperty
,都接受可选的闭包参数,这些参数可以在访问或写入属性时修改属性值。
这些参数的签名都是(propertyType)->propertyType
,其中闭包接收值并返回相同或修改后的值。
不可变属性包装器接受一个modifier
参数,该参数接收模型当前属性值,并在将其传递给调用者之前有机会修改它。这是一个可选参数,默认情况下将值原样通过。
可变属性包装器接受getModifier
和setModifier
参数,这些参数在获取模型值和设置模型值时被调用。这些都是可选参数,默认情况下将值原样通过。
模型
可变
我们使用的大多数模型对象都是不可变的,但偶尔需要可变性。可以创建一个处于可变状态的WrapModel
对象,或者制作一个可变副本。
// If you need a mutable copy
// You can initialize a model from another (with or without its mutations if it was mutable)
let mutableCust = Customer(asCopyOf: cust, withMutations: true, mutable: true)
// Or by using WrapModel's mutableCopy method which returns an Any?
let mutableCust = cust.mutableCopy() as? Customer
比较
WrapModel
遵从Equatable协议,因此相同类型的模型可以使用Swift中的==
运算符或Objective-C中的isEqual:
或isEqualToModel:
进行比较。这些比较方法的默认实现创建使用模型属性和当前数据值的字典,然后比较这些字典。
可能(并且应该)在特定模型子类中覆盖这些比较方法,以便使比较更具体于涉及的数据。
复制模型
WrapModel
遵循 NSCopying 协议,因此您可以使用 copy()
和 mutableCopy()
生成副本,但您需要将结果类型转换为 Any
。
您还可以使用复制初始化器 init(asCopyOf:WrapModel, withMutations:Bool, mutable:Bool)
来实例化一个副本。这将生成给定模型的有类型副本,您可以选择创建的模型是否是可变的,以及它是否包含对给定模型所做的任何更改。
输出
如果您需要模型的当前数据字典以用于,例如向服务器端点发送数据,则模型中的 currentModelData()
函数将构建并返回它。即使初始化模型所用的字典包含额外的数据,此函数也仅返回模型定义的属性的字典。
currentModelData()
接受两个参数,可更改字典的构建方式
withNulls: Bool
- 如果为真,模型中无值的属性将在数据字典中输出 NSNull,在转换为 JSON 时将转换为 nil 值。forOutput: Bool
- 如果为真,则仅输出模型中serializeForOutput
标志为真的属性。此参数默认为 true。
currentModelDataAsJSON(withNulls:Bool)
也可用,返回只包含模型中 serializeForOutput
标志为真的属性的 JSON 字符串。
NSCoding
WrapModel
遵循 NSCoding
协议,因此您可以使用 NSKeyedArchiver
和 NSKeyedUnarchiver
类来归档和解档模型对象。
目标
(更详细)
在Swift中声明简单
属性声明通常较短,只需使用WrapProperty
子类和关键路径。
类似直接属性的用法简单
使用基于WrapModel
的模型对象与使用直接属性非常相似。
速度 - 数据转换是懒加载的
通过推迟转换直到需要数据,WrapModel
避免了将数据字典转换成模型对象时所需要的大量时间。这对于从未被修改的模型和具有许多变换属性的模型尤其如此。
一次性定义属性(无需维护第二列表)
使用Swift 5.1,属性通过属性包装器一次性声明。这是最简单、最直接的方法,并提供在需要时通过编译器强制属性不可变的功能。
在Swift 5.1之前的版本中,可以通过值成员直接使用一次定义的属性。属性声明是自包含的,无需在一个地方进行声明以及在另一个地方规定转换方法。
如果您不能使用Swift 5.1或更高版本,也可以选择使用私有的属性定义/公共访问器声明模式(为与Objective-C兼容、编译器强制属性不可变以及其它原因),这两个相关声明紧密相关,因此若忘记了其中一个声明,则无法使用该属性。
数据类型和枚举的轻松转换
提供的WrapProperty
子类覆盖了大多数模型所需的大部分数据类型。同时,自定义类型,您也可以很轻松地创建自己的WrapProperty
子类。
子类只需提供一个将数据字典类型转换为模型类型的toModelConverter
闭包,以及一个执行相反操作的fromModelConverter
闭包。
灵活的结构
WrapModel
属性的格式不需要密切关联其数据字典的结构。属性可以轻松地深入到数据字典深层嵌套的成员中,因此在很多情况下不需要创建许多子模型类型,除非它符合您的目的。
您还可以创建属性组,以在数据字典同一级别将属性逻辑分组到子模型中。
易于调试
由于WrapModel
将原始数据字典与变更分离,因此调试得到了简化。模型在调试时甚至保留原始JSON字符串(如果是从JSON字符串或数据初始化)。
可强制执行的不可变性
作为非可变创建的模型不允许更改其值,并且在尝试对不可变的模型对象进行变化时会断言(断言)在调试时。
Objective C 兼容性
自 Swift 5.1 开始,属性包装器提供了便捷的 Objective C 兼容性。
对于 Swift 5.1 之前的版本,尽管模型必须在 Swift 中定义,但只需稍微多做一些工作,就可以通过使用私有属性定义/公共访问器模式,从 Objective C 代码中完全使用 WrapModel 对象。
集成
CocoaPods (iOS 10+, Swift 4.2+)
您可以使用 CocoaPods 将 WrapModel
添加到您的 Podfile
中来安装它。
platform :ios, '10.0'
use_frameworks!
target 'MyApp' do
pod 'WrapModel', '~> 1.0'
end
手动 (iOS 10+, Swift 4.2+)
由于 WrapModel
仅由几个 Swift 源文件组成,您可以下载它们并将它们手动编译到您的项目中。
杂项
存在一个可选的全局闭包名为 WrapPropertyKeyPathModifier
,它可以作为所有 keyPath 名称的预处理器使用。例如,如果您想自动裁剪 keyPath 值中的任何下划线和/或点,您可以指定一个如下的闭包
WrapPropertyKeyPathModifier = { keyPath in
return keyPath.trimmingCharacters(in: CharacterSet.init(charactersIn: "._"))
}
存在一个预定义的闭包,只会裁剪下划线
WrapPropertyKeyPathModifier = WrapPropertyTrimUnderscoresKeyPathModifier
最后
我编写了WrapModel
以满足1stdibs在从基于Objective C的Mantle模型过渡到以Swift为中心的模型时的所有目标。自2018年起,我们一直在1stdibs的生产iOS应用中使用WrapModel
。我们有一个相当简单的基于接口的系统,允许我们混合使用基于WrapModel
和Mantle的模型,因此我们并不被迫一次性过渡所有内容。
最初实现也做出了重要贡献的是Gal Cohen。
现在,1stdibs的整个移动工程团队都在使用和维护WrapModel
,希望您也能如此。
开发愉快!