WrapModel 1.3.6

WrapModel 1.3.6

Ken WorleyGal Cohen 维护。



WrapModel 1.3.6

  • Ken Worley

Platform License Version

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()
}

内容

  1. 根据
  2. 要求
  3. 沟通
  4. 为什么不使用 Codable?
  5. 用法
  6. 线程安全
  7. 模型属性
  8. 属性包装器 & 值修饰符
  9. 模型
  10. 目标(更深入)
    1. 在 Swift 中声明简单
    2. 易于使用
    3. 速度
    4. 一次定义属性
    5. 易于转换
    6. 灵活的结构
    7. 易于调试
    8. 可强制执行的不可变性
    9. Objective-C 兼容性
  11. 集成
  12. 其他
  13. 最后

事由

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?
}

定义模型对象 - 只使用WrapProperty对象:

也可以使用裸露的属性对象,并通过每个(仅从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

**NSNumber类型**

简称 数据类型 长名 默认值 属性包装器
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。

日期属性

WrapPropertyDateWPDate)处理通过枚举指定的几个常见的日期格式。从字符串到传入的翻译尝试首先从指定的日期类型解码,然后还尝试所有它知道的其它类型。转换回字符串始终使用指定的日期类型。这是最宽容的日期属性类型。

当前由 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

WrapPropertyDateFormattedWPDateFmt)使用一个 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
		    }
		}
	]
}

使用 WPEmbModelArrayWPOptEmbModelArray,你可以避免为这些裸封装创建模型类,而是创建一个属性,该属性简单返回模型本身的一个数组。只需为属性初始化器中的 embedPath 参数指定一个点分隔的键路径,属性对象将自动从一层或多层封装中提取模型。

客户的数组属性声明如下

@EmbModelArrayProperty("customers", embedPath:"node") var customers:[Customer]

属性组

属性组定义为子模型,但不深入数据字典的下一级。这有两个原因

  1. 如果你有一个具有大量属性(其中许多只有在特定情况下才使用)的模型,那么该模型正在创建大量可能不会使用的属性对象。即使它们位于数据字典的顶部,也可以将这些属性分组并在子模型中定义。

  2. 如果你有一个 "扁平化" 的数据模型,具有属性的逻辑子组,将它们定义为组可以使访问更加合理。

例如,对于一个看起来像这样的扁平数据字典

{
  "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 或更高版本。

提供了两个泛型属性包装器 ROPropertyRWProperty,可以用来包装任何 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 允许修改属性值(可变)。

值修饰符参数

每个提供的属性包装器,从更通用的ROPropertyRWProperty,到更具体的如StrPropertyIntProperty,都接受可选的闭包参数,这些参数可以在访问或写入属性时修改属性值。

这些参数的签名都是(propertyType)->propertyType,其中闭包接收值并返回相同或修改后的值。

不可变属性包装器接受一个modifier参数,该参数接收模型当前属性值,并在将其传递给调用者之前有机会修改它。这是一个可选参数,默认情况下将值原样通过。

可变属性包装器接受getModifiersetModifier参数,这些参数在获取模型值和设置模型值时被调用。这些都是可选参数,默认情况下将值原样通过。

模型

可变

我们使用的大多数模型对象都是不可变的,但偶尔需要可变性。可以创建一个处于可变状态的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() 接受两个参数,可更改字典的构建方式

  1. withNulls: Bool - 如果为真,模型中无值的属性将在数据字典中输出 NSNull,在转换为 JSON 时将转换为 nil 值。
  2. forOutput: Bool - 如果为真,则仅输出模型中 serializeForOutput 标志为真的属性。此参数默认为 true。

currentModelDataAsJSON(withNulls:Bool) 也可用,返回只包含模型中 serializeForOutput 标志为真的属性的 JSON 字符串。

NSCoding

WrapModel 遵循 NSCoding 协议,因此您可以使用 NSKeyedArchiverNSKeyedUnarchiver 类来归档和解档模型对象。

目标

(更详细)

在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+)

您可以使用 CocoaPodsWrapModel 添加到您的 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,希望您也能如此。

开发愉快!

Ken Worley