最简单易用的客户端应用程序的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是一种类型别名,代表两种类型: FirabaseFetchable和Makeable 。
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保存,获取和删除您的类型的实例的所有内容。
您可以通过此链接中的应用示例检查项目源。