AnyPropertyMapping

CocoaPods Build SPM

AnyPropertyMapping 提供了一种便捷的方式来映射类类型的属性,并在两个方向上对它们的实例执行操作。 映射完全基于键路径。 开箱即用地支持可选属性。 由于该库严重依赖键路径:如果键路径语法支持,该库也将支持。

灵感来自于快速创建模拟对象或不同层(网络或数据,甚至 UI)之间的中间对象的需要。 这里有一套相当全面但不完整的实用函数可用,并且核心功能(特别是处理键路径和类型擦除)已通过单元测试覆盖。

此库使用泛型并严重依赖类型擦除:仅限 Swift 5+,不确定在较旧的 Swift 版本中这在概念上是否可行。 Swift 的静态类型使这相当... 让我们委婉地称之为:不平凡。

安装

将以下依赖项添加到您的 Package.swift 文件中

.package(url: "https://github.com/snofla/AnyPropertyMapping.git", from: "1.1.0")

或者,如果您使用 CocoaPods,请将以下行添加到您的 Podfile

pod 'AnyPropertyMapping'

用法

最简单的情况:映射相同类型的对象属性

最简单的情况是左侧对象属性与右侧对象属性的类型相同。 任何一侧的属性是否为可选(并且对象本身甚至可能是同一类型)都无关紧要。

在两个类之间设置映射表,它就会自动工作。 保证!

开玩笑的。

假设您在网络层中有一个类

class UserAdressFromNetwork {
  var userName: String?
  var address: String
  var id: Int
}

您自己的层可能具有略有不同的实现

class UserAddressInUI {

  class Technical {
    var id: Int = -1
  }
  
  init(id: Int, familyName: String, address: String?) {    
    self.technical.id = id
    self.familyName = familyName
    self.address = address
  }
  
  var familyName: String
  var address: String?  
  var technical = Technical()  
}

特别注意:这些类使用可选类型,并且涉及一个内部类。

AnyPropertyMapping 提供了四个操作,但最重要的两个是

您可以按照如下方式设置映射,并调用映射操作

let mapping: [AnyPropertyMapping] = [
  // Remember: the direction is ←
  PropertyMapping(\UserAddressInUI.familyName, \UserAdressFromNetwork.userName),
  PropertyMapping(\UserAddressInUI.address, \UserAddressFromNetwork.address),
  PropertyMapping(\UserAddressInUI.technical.id, \UserAddressFromNetwork.id)
]

let a: UserAddressInUI = ...
let b: UserAdressFromNetwork = ...
// adapt from network layer data ←
mapping.forEach { $0.adapt(to: a, from: b) }
...
// make changes to a
...
// apply after changes → (move back into rhs):
mapping.forEach { $0.apply(from: a, to: b)  }

基本上就是这样。

一般情况:映射不同类型的对象属性

更常见的情况是目标和源具有不同类型的字段; 在这种情况下,必须进行某种转换。 AnyPropertyMapping 也通过一种名为 PropertyTransformer 的特殊泛型类来支持这一点。 PropertyMapping 类具有接受 PropertyTransformer 实例的构造函数。

PropertyTransformer 构造函数接受两个闭包

以下是将 Double 转换为 Int 的转换器的示例

class YourTransformers {
    /// Converts a double to an int
    public static let intDouble = PropertyTransformer<Int, Double>(adapt: { double in
        return Int(double.rounded())
    } apply: { int in
        return Double(int)
    })
}

让我们演示如何使用转换器设置映射并将其与实际类一起使用

class A {
  var optionalInt: Int? = 1
}

class B {
  var nonOptionalDouble: Double = 3.0
}

let mapping: [AnyPropertyMapping] = [
    PropertyMapping(
      \A.optionalInt, 
      \B.nonOptionalDouble, 
      transformer: YourTransformers.intDouble
    )  
]
let a = A()
let b = B()
mapping.adapt(to: a, from: b)
// do something
mapping.apply(from: a, to: b)

如果操作涉及可选属性(任一侧),则会使用默认值隐式转换这些属性。 这就是为什么转换器不需要以任何特殊方式处理可选类型的原因。

请注意转换器的 adaptapply 操作如何紧密地镜像属性映射的操作。 如果您有两个类 AB,则 adapt 始终具有 ← 方向(rhs 到 lhs),而 apply 则相反(→,lhs 到 rhs)

为了处理相反的情况,PropertyTransformer 提供了 inverted() 操作。 使用上面示例中的类,我们也可以像这样定义一个反向映射

// Map lhs B to rhs A
let mappingBA: [AnyPropertyMapping] = [
    PropertyMapping(
      \B.nonOptionalDouble, 
      \A.optionalInt,       
      transformer: YourTransformers.intDouble.inverted()
    )  
]

// NOTE 1: Another version would look like this, and inverts
// an entire property mapping:
let mappingBA_Alt1: [AnyPropertyMapping] = [
    PropertyMapping(
      \B.nonOptionalDouble, 
      \A.optionalInt,       
      transformer: YourTransformers.intDouble
    ).inverted()  
]

// NOTE 2: Yet another version would look like this, and inverts
// the entire array of property mappings:
let mappingBA_Alt2: [AnyPropertyMapping] = [
    PropertyMapping(
      \B.nonOptionalDouble, 
      \A.optionalInt,       
      transformer: YourTransformers.intDouble
    )
].inverted()  

AnyPropertyMapping 没有提供很多转换器; 可能的转换器太多,而且肯定会受到开发人员用例(和想象力)的限制。 但是,可以轻松创建新的转换器:考虑将日期格式字符串转换为实际的 Swift Date,或将自定义类转换为标量(例如,类特定的哈希)。

便捷函数

Sequence 的一个扩展,其中 Element 是 AnyPropertyMapping,提供了以下函数

func adapt(to:from:) // adapts property mappings from RHS to LHS - can both be arrays
func apply(from:to:) // adapts property mappings to LHS from RHS - can both be arrays
func differs(_:_:) -> Bool  // checks if there are differences in properties mapped between LHS and RHS
func inverted() // returns the inverse of a sequence of property mappings
func differences(_:_:) -> [(left: AnyKeyPath, right: AnyKeyPath)]? // returns the differences
func differenceIndex(_:_:) -> IndexSet // returns the indices that are different

Sequence 的一个扩展,其中 Element 是 LHS 和 RHS 类的元组,并且您提供了一个 Sequence 的映射

Sequence<(LHS, RHS)> func adapt(mappings:) // adapts mappings to a sequence of tuples of LHS and RHS
Sequence<(LHS, RHS)> func apply(mappings:) // applies mappings to a sequence of tuples of LHS and RHS

更多文档即将推出,但也可以查看单元测试。

注意

作者

Alfons Hoogervorst

鸣谢

Elastique (https://www.elastique.nl); 我们做真正有趣的事情。

许可证

MIT。

任何无意中提及的商标均为其各自所有者的财产。