最简单易用的客户端应用程序的Swift Generics和元编程

使用元编程和泛型来构建最易于使用的客户端API

该项目的目标是避免通常在客户端应用程序上需要的样板代码。 您将能够避免将本地对象转换为所需的服务器端对象,反之亦然。 您还将免费获得所有服务器通信方法。 只需创建一个新类型并与服务器进行通信即可。

例如,最后,您将能够创建一个符合我们的帮助协议FirebaseFetchable的新类,如下所示:

 最终课程宠物:FirebaseFetchable { 
  //来源:忽略 
  var firebaseId:字符串=“” 
  //来源:忽略 
  var isCompleted:Bool = false 
 变量名称:字符串=“” 
 在里面() {} 
  } 

您将能够使用自动生成的PetManager类中的自动生成的方法与服务器进行通信,如下所示:

 让fido = Pet() 
  fido.name =“ Fido” 
  PetManager.shared.save(fido) 

这会将Pet对象保存到您的远程数据库中。

您还可以自动生成用于从数据库中获取,更新和删除的方法。 因此,实际上这仅是创建新类型matter的问题。

在此项目中,我使用了Firebase,因为它是快速项目的非常简单的设置。 对于元编程,我使用了Sourcery,这是一个出色的代码生成工具。

设定

首先,请在Xcode中创建一个单视图应用程序。 然后,您需要设置Firebase。 转到firebase网站,获取您的plist文件以及仪表板设置。 非常容易和快速。 之后,下载plist文件并将其附加到项目。 如果需要帮助,请查看他们的教程。

然后,创建一个Pod文件,包括Firebase和Sourcery并安装它们。 如果您需要Pod的帮助,请查看此链接。 您的pod文件将如下所示:

 目标“您的项目名称” 
use_frameworks!
  #Your_Project_Name的广告连播 
pod'Sourcery'
pod'Firebase / Database'
结束

在UIApplicationDelegate中,确保导入Firebase模块:

 导入Firebase 

并在同一文件中配置Firebase共享实例:

  func application(_ application:UIApplication,didFinishLaunchingWithOptions launchOptions:[UIApplicationLaunchOptionsKey:Any]?)->布尔{ 
  FirebaseApp.configure() 
 返回真 
  } 

现在您已完成所有设置,让我们快速考虑一下基本api客户端上的要求:

您可能需要将几个对象放到数据库中。 为了将这些对象与服务器连接,您必须以服务器期望的正确方式转换它们。 例如,Firebase在保存内容时需要某种Json对象。 在获取对象时,还需要将这些对象转换回去。

您还需要保存,获取,删除方法以与服务器进行通信。

当您的项目开始发展,并且您有许多不同的对象并且它们相互关联时,所有这些工作将变得非常令人沮丧。

急需救援!

Sourcery是使我们能够自动生成代码的工具。 在这个项目中,我们将帮助我们避免api连接带给我们的所有重复样板。

我们可以创建一个脚本来为我们完成所有这些工作。

在此之前,创建一个协议并将其命名为FirebaseFetchable:

 进口基金会 
 协议FirebaseFetchable { 
  var firebaseId:字符串{获取设置} 
  var isCompleted:布尔{获取设置} 
  } 

该协议声明了两个类型必须符合的变量:firebaseId和isCompleted。 firebaseId将是对象的ID,在本教程中您无需担心isCompleted变量。

该协议的主要用途是标记我将使用Firebase的所有类型。 例如,Pet类:

 最终课程宠物:FirebaseFetchable { 
  //来源:忽略 
  var firebaseId:字符串=“” 
  //来源:忽略 
  var isCompleted:Bool = false 
 变量名称:字符串=“” 
 在里面() {} 
  } 

在本教程的结尾,我将讨论注释的必要性,属性的默认值以及空的初始化程序。

遵循协议并能够构建项目之后,现在我可以继续创建第二个协议:Makeable。

 进口基金会 
 协议Makeable { 
  func toDictionary()-> [String:任何] 
 静态函数make(来自字典:[String:Any])-> Self 
 变异功能更新(其他:自我) 
  } 

该协议将声明将对象转换为Firebase期望并返回的方法。 但是,我们将让Sourcery为我们实现这些方法。

创建一个名为template的文件夹,然后在其上放置一个名为Firebase.stencil的新文件。

该文件将包含以下代码

  {%用于types.implementing.FirebaseFetchable%的类型} 
  // — — — — {{type.name}}相关的生成代码— — — — — — — — — — // // 
  struct {{type.name}}键{ 
 静态let tableName =“ {{type.name}}” 
 静态让firebaseId =“ firebaseId” 
  {%为type中的变量。variables%} 
  {%ifnot variable.annotations.ignore%} 
 静态let {{variable.name}} =“ {{variable.name}}” 
  {% 万一 %} 
  {%endfor%} 
  } 
 扩展名{{type.name}}:可实现{ 
  func toDictionary()-> [String:Any] { 
  var dict:[String:Any] = [:] 
  {%为type中的变量。variables%} 
  {%,如果变量|实现:“ FirebaseFetchable”%} 
  dict [{{type.name}}键。{{variable.name}}] = {{variable.name}}。firebaseId 
  {%elif variable.isArray和variable.typeName.array.elementType.implements.FirebaseFetchable%} 
  var {{variable.name}}参考:[String:Bool] = [:] 
  {{variable.name}}。forEach {{{variable.name}} Refs [$ 0.firebaseId] = true} 
  dict [{{type.name}}键。{{variable.name}}] = {{variable.name}}引用 
  {%else%} 
  {%ifnot variable.annotations.ignore%} 
  dict [{{type.name}}键。{{variable.name}}] = {{variable.name}} 
  {% 万一 %} 
  {% 万一 %} 
  {%endfor%} 
  dict [{{type.name}} Keys.firebaseId] = self.firebaseId 
 返回字典 
  } 
  {%,如果键入| struct%} 
 变异func update(other:{{type.name}}){ 
 自我=其他 
  } 
  {%else%} 
  func update(other:{{type.name}}){ 
  self.isCompleted = true 
  {%为type中的变量。variables%} 
  {%ifnot variable.annotations.ignore%} 
 自我。{{variable.name}} =其他。{{variable.name}} 
  {% 万一 %} 
  {%endfor%} 
  } 
  {% 万一 %} 
  {%,如果键入| struct%} 
 静态函数make(来自字典:[String:Any])-> {{type.name}} { 
  var object = self.init() 
  {%else%} 
  {如果type.isFinal为%,则为%} 
 静态函数make(来自字典:[String:Any])-> {{type.name}} { 
  {%else%} 
 静态函数make(来自字典:[String:Any])-> Self { 
  {% 万一 %} 
 让对象= self.init() 
  {% 万一 %} 
  object.firebaseId = dictionary [{{type.name}} Keys.firebaseId]! 串 
  object.isCompleted = true 
  {%为type.variables | instance |!中的变量的注释:“忽略”%} 
  {%,如果变量|实现:“ FirebaseFetchable”%} 
  let element = {{variable.typeName}}() 
  element.firebaseId = dictionary [{{type.name}} Keys。{{variable.name}}]为! 串 
 对象。{{variable.name}} =元素 
  {%elif variable.isArray和variable.typeName.array.elementType.implements.FirebaseFetchable%} 
 让{{variable.name}} Refs = dictionary [{{type.name}} Keys。{{variable.name}}]视为?  [String:Bool] ??  [:] 
 对象。{{variable.name}} = {{variable.name}} Refs.map {(参考) 
  var element = {{variable.typeName.array.elementType.name}}() 
  element.firebaseId = reference.key 
 返回元素 
  } 
  {%else%} 
 对象。{{variable.name}} =字典[{{type.name}}键。{{variable.name}}]为!  {{variable.typeName}} 
  {% 万一 %} 
  {%endfor%} 
 返回对象 
  } 
  } 
  {%,如果键入| struct%} 
 扩展名{{type.name}} { 
 在里面() { 
  {%为type中的变量。variables%} 
 自我。{{variable.name}} = {{variable.typeName}}() 
  {%endfor%} 
  } 
  } 
  {% 万一 %} 
  // — — — — {{type.name}}代码的结尾— — — — — — — — — — // // 
  {%endfor%} 

我正在使用一种名为Stencil的语言来创建Sourcery模板,但是使用它非常简单。

遍历文件的重要部分,您会注意到我们如何对 实现我们的协议FirebaseFetchable的所有类型。 如前所述,该协议基本上用于标记我们要与Firebase通信的类型。

  {%用于types.implementing.FirebaseFetchable%的类型} 

目前,唯一实现FirebaseFetchable协议的类型是Pet类。 在其余的代码之后,您可以看到我们创建了一个名为PetKeys的结构。

  struct {{type.name}}键{ 

如前所述,Firebase是一个基于JSON的数据库 ,因此我创建了此结构来存储我们类型属性的所有键。

然后,我们创建Pet类型的扩展,使其符合我们的协议Makeable

 扩展名{{type.name}}:可实现{ 

如前所述,该协议声明了将对象转换为Firebase期望并返回所需的所有方法。 我们在模板中实现了所有这些方法,因此实现FirebaseFetchable的所有类型都无需在以后实现。

在toDictionary函数中:

  func toDictionary()-> [String:Any] { 

我们创建一个字典,然后遍历当前类型的每个属性,在刚创建的Type.NameKeys结构中搜索相应的Key ,然后返回此字典。 此方法用于将我们的对象转换为Firebase期望值。

然后我们实现方法make(from dictionary:[String:Any])

  {%,如果键入| struct%} 
 静态函数make(来自字典:[String:Any])-> {{type.name}} { 
  var object = self.init() 
  {%else%} 
  {如果type.isFinal为%,则为%} 
 静态函数make(来自字典:[String:Any])-> {{type.name}} { 
  {%else%} 
 静态函数make(来自字典:[String:Any])-> Self { 

此方法将从字典对象(我们从firebase获得的内容)转换为当前的Type。

同样,我们使用Type.nameKeys结构从字典中获取每个值并返回我们类型的实例。

— —您可以在声明中看到很多IF和ELSE语句。 我用它们来处理结构和类。 另一个IF语句与类继承有关。 如果该类不是最终的(可以被子类化),则需要确保我们处理的是正确的类型,而不是超级类。 因此,return语句必须为Self类型。 我可以通过使所有类都返回Self来避免一条IF语句,但这只是一个好看的问题,因为我发现在方法声明中返回实际的类型名称更为美观—

大! 现在该看一些Sourcery魔术了。 要让Sourcery从我们的模板自动生成代码,有两种方法。 我强烈建议您查看他们的文档以查看他们所提供的所有内容。

在本教程中,每次构建项目时,我都会运行Sourcery。 为此,请在XCode中选择目标,转到“构建阶段”选项卡,并添加一个新脚本以执行以下操作:

  $ PODS_ROOT / Sourcery / bin / sourcery --sources ./  --templates ./ApiClient/Templates/ --output ./  / AutoGenerated 

现在,每次构建时,编译器都会在template文件夹中查找Sourcery模板,然后将生成的代码输出到一个名为AutoGenerated的文件中。

构建项目后,您将拥有一个名为AutoGenerated的全新文件,其中包含以下内容:

  // — — — —宠物相关的生成代码— — — — — — — — — // 
  struct PetKeys { 
 静态let tableName =“宠物” 
 静态让firebaseId =“ firebaseId” 
 静态让名称=“名称” 
 静态让年龄=“年龄” 
 静态让petType =“ petType” 
  } 
 扩展宠物:可制作{ 
  func toDictionary()-> [String:Any] { 
  var dict:[String:Any] = [:] 
  dict [PetKeys.name] =名称 
  dict [PetKeys.age] =年龄 
  dict [PetKeys.petType] = petType。 
  dict [PetKeys.firebaseId] = self.firebaseId 
 返回字典 
  } 
  func update(其他:宠物){ 
  self.isCompleted = true 
  self.name =其他名称 
 自我年龄=其他年龄 
  self.petType = other.petType 
  } 
 静态函数make(来自字典:[String:Any])-> Pet { 
 让对象= self.init() 
  object.firebaseId = dictionary [PetKeys.firebaseId]一样! 串 
  object.isCompleted = true 
  object.name = dictionary [PetKeys.name]为! 串 
  object.age = dictionary [PetKeys.age]为! 整数 
  object.petType = dictionary [PetKeys.petType]为! 串 
 返回对象 
  } 
  // — — — —宠物代码的结尾— — — — — — — — — — // 

那很棒! 现在,我们拥有将Pet类型的对象转换为字典,并将字典转换为Pet实例所需的一切。 为了使这些方法具有不同的类型,只需使该类型实现FirebaseFetchable协议即可

API

现在是时候编写与Firebase通信的方法了。

为此,创建一个名为FirabaseCrudable的协议

  typealias FirebaseModel = FirebaseFetchable和Makeable 
 协议FirebaseCrudable { 
  relatedtype模型:FirebaseModel 
  var ref:DatabaseReference {get} 
  var tableName:字符串{get} 
  func save(_模型:inout模型,完成:(((Result )-> Void)?) 
  func fetch(byId id:字符串,完成:@escaping(Result )->无效) 
  func ifNeeded(_模型:模型,完成:@转义(Result )->无效) 
  func remove(_模型:模型,完成:((Result )-> Void)?) 
  func map(model:Model)-> [String:任何] 
  } 
 枚举Result  { 
 成功案例(T) 
 大小写错误(错误) 
  } 

我还扩展了协议,使其具有这些方法的默认实现:

 扩展名FirebaseCrudable { 
  var ref:DatabaseReference { 
 返回Database.database()。reference() 
  } 
  var tableName:字符串{ 
 返回字符串(描述:Model.self) 
  } 
  func map(model:Model)-> [String:Any] { 
 返回[“ \(tableName)/ \(model.firebaseId)”:model.toDictionary()] 
  } 
  func save(_模型:inout模型,完成:(((Result )-> Void)?){ 
 如果model.firebaseId ==“” { 
 让child = ref.child(tableName).childByAutoId() 
  model.firebaseId = child.key 
  } 
  ref.updateChildValues(map(model:model)){(错误,_)在 
 防护错误==无其他{ 
 完成?(.error(错误!)) 
 返回 
  } 
 完成?(.success(())) 
  } 
  } 
  func fetch(byId id:字符串,完成:@escaping(Result )-> Void){ 
  ref.child(tableName).child(id).observeSingleEvent(of:.value,with:{(snapshot)in 
 如果snapshot.hasChildren(){ 
 让dict = self.convertToDictionary(fromDataSnapshot:快照) 
 让模型= Model.make(来自:dict) 
 完成(。成功(模型)) 
  }其他{ 
 完成(.error(ManagerError.notFound(“从\(id)tableName中找不到值:\(self.tableName)”)))) 
  } 
  }){(错误) 
 完成(.error(错误)) 
  } 
  } 
  func ifNeeded(_模型:模型,完成:@转义(Result )->无效){ 
  var modelRef =模型 
 卫队!modelRef.isCompleted else { 
 完成(.success(nil)) 
 返回 
  } 
  fetch(byId:modelRef.firebaseId){(结果)在 
 切换结果{ 
 大小写.error(让错误): 
 完成(.error(错误)) 
 案例.success(let fetchedModel): 
  modelRef.update(other:fetchedModel) 
 完成(.success(modelRef)) 
  } 
  } 
  } 
  func convertToDictionary(fromDataSnapshot dataSnapshot:DataSnapshot?)-> [字符串:任意] { 
 保护让快照= dataSnapshot其他{ 
 返回[:] 
  } 
  var dictionary = snapshot.value as?  [String:Any] ??  [:] 
 字典[“ firebaseId”] = snapshot.key 
 返回字典 
  } 
  func remove(_模型:模型,完成:(((Result )-> Void)?){ 
  ref.child(tableName).child(model.firebaseId).removeValue {(错误,_)在 
 如果让错误=错误{ 
 完成?(.error(错误)) 
  }其他{ 
 完成?(.success(())) 
  } 
  } 
  } 
  } 
 枚举ManagerError:错误{ 
  case notFound(字符串) 
  var localizedDescription:字符串{ 
 切换自我{ 
 案例.notFound(让消息): 
 返回讯息 
  } 
  } 
  } 

我们使用此协议的目标是让Firebase使用CRUD的每种类型都有一个类,该类将管理每种类型与服务器的通信。 因此,每种类型都有其自己的类,这些类将实现FirebaseCrudable协议,并将具有其所有扩展功能。

现在来看一下这里发生的重要部分:

我们的协议声明了一个FirabaseModel类型的relatedType 模型

  relatedtype模型:FirebaseModel 

FirebaseModel是一种类型别名,代表两种类型: FirabaseFetchableMakeable

  typealias FirebaseModel = FirebaseFetchable和Makeable 

我们使用associatedType来告诉我们的类,我们正在处理什么类型。 我们还需要确保该类型具有firebaseId(这就是FirebaseFetchable协议的原因),并且能够将其转换为Firebase期望值并返回(这就是Makeable协议的原因)。

在扩展中,您可以查看正在发生的事情,但总而言之,这只是我对save,remove和fetch方法的实现。 如果您需要帮助,可以查看Firebase文档。

下一步是创建一个单例类,该类将管理我们的Pet类与Firebase的通信。

为此,我们回到Sourcery模板并添加以下几行:

 最终课程{{type.name}}管理者:FirebaseCrudable { 
  typealias型号= {{type.name}} 
  static let shared = {{type.name}} Manager() 
  } 

上线之前:

  // — — — — {{type.name}}代码的结尾— — — — — — — — — — //评论 

这将创建符合FirebaseCrudable协议的经理类。

构建代码之后,我们可以看到在AutoGenerated文件中创建了一个名为PetManager的新类。

现在,如果要在Firebase中保存Pet的新实例,我们要做的就是从我们的PetManager 共享实例中调用save方法,就是这样。

 让fido = Pet() 
  fido.name =“ Fido” 
  PetManager.shared.save(fido) 

我想拥有一个struct Person并将其保存在Firebase中:

  struct Person:FirebaseFetchable { 
  //来源:忽略 
  var firebaseId:字符串 
  //来源:忽略 
  var isCompleted:布尔 
 变量名称:字符串 
  } 

现在,您只需构建即可执行以下操作:

 让荷马= Person() 
  person.name =“荷马” 
  PersonManager.shared.save(homer) 

— — — — — — — — — — — — — — — — — — — — — —

最终指示

对于您在此处创建的以下 ,有一些预防措施:

  • 确保它正在实现FirebaseFetchable
  • 您声明的每个属性都必须具有默认值 。 例:
  var name =“” 
  • 创建一个空的init
 在里面() {} 

这是必需的,因为否则我们将不得不为每个类创建一个init方法,并且在这种情况下很难在Sourcery模板上通用。

如果您在Sourcery模板中检查make(from dictionary [String:Any])方法的实现,您会发现我们利用空的init来创建此类的实例,然后对for的每个属性进行一个for循环。类型并将其分配给其各自的值。

我们对结构没有这种义务。 我们不需要为每个属性设置默认值,因为我们免费获得了成员初始化器。 我们也不需要创建一个空的初始化器,因为与类相比,对于结构,我们可以在扩展内部包含一个初始化器。 这就是我们在Sourcery模板中使用以下代码进行的操作:

  {%,如果键入| struct%} 
 扩展名{{type.name}} { 
 在里面() { 
  {%为type中的变量。variables%} 
 自我。{{variable.name}} = {{variable.typeName}}() 
  {%endfor%} 
  } 
  } 
  {% 万一 %} 

对于结构和类,还有一个额外的步骤,即使用以下命令注释FirebaseFetchable变量:

  //来源:忽略 

当将对象覆盖到字典并返回时,该属性用于忽略这些属性。

总结 (最后😅)

这可能要消化很多,但结果却很酷。 现在,我们可以创建一个新类型并将其简单地符合FirebaseFetchable。 构建项目后,您将拥有一个自动生成的类,该类将包含您需要使用firebase保存,获取和删除您的类型的实例的所有内容。

您可以通过此链接中的应用示例检查项目源。