Swift Generics and metaprogramming for the easiest-to-use client side app
Using meta programming and generics to build the easiest-to-use client API
The goal of this project is to avoid boilerplate code usually required on an client-side application. You’ll be able to avoid casting a local object to its required server-side object and vice versa. You will also get all the server-communication methods for free. It will just be a matter of creating a new type and get to communicate with the server.
For example, at the end you will be able to create a new class that conforms to our helper protocol FirebaseFetchable like so:
final class Pet: FirebaseFetchable { // sourcery: ignore var firebaseId: String = “” // sourcery: ignore var isCompleted: Bool = false var name: String = “” init() {}}
And you will be able to communicate with the server with automatically generated methods from an automatically generated PetManager class, like so:
let fido = Pet()fido.name = “Fido”PetManager.shared.save(fido)
This will save a Pet object into your remote database.
You also have automatically generated methods to fetch, update and remove from the database. So it will be really just a matter of creating a new type 🤩.
In this project I used Firebase as it is a very easy setup for quick projects. For meta programming I used Sourcery, which is a great code generation tool.
Setup
For starters, create a single view application in Xcode. You will then need to setup Firebase. Go to the firebase website and get your plist file as well as the dashboard setup. Very easy and quick. After that, download the plist file and attach it to the project. If you need help, check out their tutorial.
Then, create a pod file, include Firebase and Sourcery and install them. If you need help with Pods, checkout this link. Your pod file will look like this:
target 'Your_Project_Name' do
use_frameworks!# Pods for Your_Project_Name
pod 'Sourcery'
pod 'Firebase/Database'
end
In your UIApplicationDelegate make sure you import the Firebase module:
import Firebase
And in the same file configure a Firebase shared instance:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {FirebaseApp.configure()return true}
Now that you have everything set up, let’s do a quick thinking about what is required on a basic api client:
You’d probably have a couple of objects that you want to CRUD into your database. In order to connect those objects with the server you would have to convert them in the correct way that your server expects. For example, Firebase expects some sort of Json objects when saving stuff. You would also need to convert these objects back when fetching objects.
You would also need save, fetch, remove methods in order to communicate the server.
All this work gets very frustrating when your project starts growing and you have a lot of different objects and they relate to each other.
Sourcery to the rescue!
Sourcery is a tool that allow us to generate code automatically. In this project we it to help us avoid all the boilerplate that the api connection brings us.
We can create a script that will do all of that work for us.
Before that, create a protocol and name it FirebaseFetchable:
import Foundationprotocol FirebaseFetchable { var firebaseId: String { get set } var isCompleted: Bool { get set }}
This protocol declares two variables that the types need to conform: firebaseId and isCompleted. The firebaseId will be the id of the object, and the isCompleted variable you don't need to worry on this tutorial.
The main porpouse of this protocol is to tag all of the types that I’ll work with Firebase. So for example, the class Pet:
final class Pet: FirebaseFetchable { // sourcery: ignore var firebaseId: String = “” // sourcery: ignore var isCompleted: Bool = false var name: String = “” init() {}}
I'll be covering the necessity of the comments, the default values for the properties and the empty initializer at the end of the tutorial.
After conforming to the protocol and being able to build the project, now I can go ahead and create a second protocol: Makeable.
import Foundationprotocol Makeable { func toDictionary() -> [String: Any] static func make(from dictionary: [String: Any]) -> Self mutating func update(other: Self)}
This protocol will declare the methods to convert the object to firebase expectations and back. But we will have Sourcery implementing those methods for us.
Create a folder named templates and then put a new file called Firebase.stencil on it.
This file will contain the following code
{% for type in types.implementing.FirebaseFetchable %}// — — — — {{ type.name }} related generated code — — — — — — — — — //struct {{ type.name }}Keys { static let tableName = “{{ type.name }}” static let firebaseId = “firebaseId” {% for variable in type.variables %} {% ifnot variable.annotations.ignore %} static let {{ variable.name }} = “{{ variable.name }}” {% endif %} {% endfor %}}extension {{ type.name }}: Makeable { func toDictionary() -> [String: Any] { var dict: [String: Any] = [:] {% for variable in type.variables %} {% if variable|implements:”FirebaseFetchable” %} dict[{{ type.name }}Keys.{{ variable.name }}] = {{ variable.name }}.firebaseId {% elif variable.isArray and variable.typeName.array.elementType.implements.FirebaseFetchable %} var {{ variable.name }}Refs: [String: Bool] = [:] {{ variable.name }}.forEach{ {{ variable.name }}Refs[$0.firebaseId] = true } dict[{{ type.name }}Keys.{{ variable.name }}] = {{ variable.name }}Refs {% else %} {% ifnot variable.annotations.ignore %} dict[{{ type.name }}Keys.{{ variable.name }}] = {{ variable.name }} {% endif %} {% endif %} {% endfor %} dict[{{ type.name }}Keys.firebaseId] = self.firebaseId return dict } {% if type|struct %} mutating func update(other: {{ type.name }}) { self = other } {% else %} func update(other: {{ type.name }}) { self.isCompleted = true {% for variable in type.variables %} {% ifnot variable.annotations.ignore %} self.{{ variable.name }} = other.{{ variable.name }} {% endif %} {% endfor %} } {% endif %} {% if type|struct %} static func make(from dictionary: [String: Any]) -> {{ type.name }} { var object = self.init() {% else %} {% if type.isFinal %} static func make(from dictionary: [String: Any]) -> {{ type.name }} { {% else %} static func make(from dictionary: [String: Any]) -> Self { {% endif %} let object = self.init() {% endif %} object.firebaseId = dictionary[{{ type.name }}Keys.firebaseId] as! String object.isCompleted = true {% for variable in type.variables|instance|!annotated:”ignore” %} {% if variable|implements:”FirebaseFetchable” %} let element = {{ variable.typeName }}() element.firebaseId = dictionary[{{ type.name }}Keys.{{ variable.name }}] as! String object.{{ variable.name }} = element {% elif variable.isArray and variable.typeName.array.elementType.implements.FirebaseFetchable %} let {{ variable.name }}Refs = dictionary[{{ type.name }}Keys.{{ variable.name }}] as? [String: Bool] ?? [:] object.{{ variable.name }} = {{ variable.name }}Refs.map { (reference) in var element = {{ variable.typeName.array.elementType.name }}() element.firebaseId = reference.key return element } {% else %} object.{{ variable.name }} = dictionary[{{ type.name }}Keys.{{ variable.name }}] as! {{ variable.typeName }} {% endif %} {% endfor %} return object }}{% if type|struct %}extension {{ type.name }} { init() { {% for variable in type.variables %} self.{{ variable.name }} = {{ variable.typeName }}() {% endfor %} }}{% endif %}// — — — — End of {{ type.name }} code — — — — — — — — — //{% endfor %}
I am using a language called Stencil for creating the Sourcery templates, but it is pretty simple to get use to it.
Going over the important parts of the file, you can notice how we do a for-loop on all the types that are implementing our protocol FirebaseFetchable. As I mentioned before, this protocol is basically used to tag the types that we want to commnunicate with Firebase.
{% for type in types.implementing.FirebaseFetchable %}
For now the only type implementing the FirebaseFetchable protocol is the class Pet. Following the rest of the code you can see that we create a struct called PetKeys
struct {{ type.name }}Keys {
As I mentioned before, Firebase is a JSON-based database, so I create this struct to store all the keys of our type properties.
We then create an extension of our type Pet, making it conform to our protocol Makeable.
extension {{ type.name }}: Makeable {
As I mentioned before, this protocol declares all the methods necessary to convert our objects to firebase expectations and back. We implement all of those methods in the template, so all types that implement FirebaseFetchable won’t need to implement it later.
In the toDictionary function:
func toDictionary() -> [String: Any] {
We create a dictionary, then go over each property of the current type, search for the corresponding Key in the Type.NameKeys struct we just created, and return this dictionary. This method is used to convert our object to the firebase expectations.
Then we implement the method make(from dictionary: [String: Any])
{% if type|struct %} static func make(from dictionary: [String: Any]) -> {{ type.name }} { var object = self.init(){% else %}{% if type.isFinal %} static func make(from dictionary: [String: Any]) -> {{ type.name }} {{% else %} static func make(from dictionary: [String: Any]) -> Self {
This method will convert from a dictionary object (What we get from firebase) to our current Type.
So again, we use our Type.nameKeys struct to get each value from our dictionary and return an instance of our type.
— — You can see a lot of IF and ELSE statements in the declaration. I use them to be able to work with structs and classes. The other IF statement is related to a class inheritance. If the class is not final (can be subclassed), I need to make sure we are dealing with the correct type and not it’s super class. So the return statement has to be of type Self. I could avoid one IF statement by making all classes return Self but it’s just a matter of good looks as I find more pretty to return the actual type name in the method declaration — —
Great! Now is time to see some Sourcery magic. To have sourcery generating the code automatically from our template there are a couple of ways. I strongly encourage you to check out their documentation to see all they have to offer.
In this tutorial I have Sourcery running every time we build our project. To do so, select your target in XCode, go to the Build Phase tab, and add a new script to be run with the following:
$PODS_ROOT/Sourcery/bin/sourcery --sources ./<Your_Project_Name> --templates ./ApiClient/Templates/ --output ./<Your_Project_Name>/AutoGenerated
Now every time we build, the compiler will look for Sourcery templates in the template folder, and then output the generated code into a file called AutoGenerated.
After you build the the project you will have a brand new file called AutoGenerated with the following content:
// — — — — Pet related generated code — — — — — — — — — //struct PetKeys { static let tableName = “Pet” static let firebaseId = “firebaseId” static let name = “name” static let age = “age” static let petType = “petType”}extension Pet: Makeable { func toDictionary() -> [String: Any] { var dict: [String: Any] = [:] dict[PetKeys.name] = name dict[PetKeys.age] = age dict[PetKeys.petType] = petType. dict[PetKeys.firebaseId] = self.firebaseId return dict}func update(other: Pet) { self.isCompleted = true self.name = other.name self.age = other.age self.petType = other.petType}static func make(from dictionary: [String: Any]) -> Pet { let object = self.init() object.firebaseId = dictionary[PetKeys.firebaseId] as! String object.isCompleted = true object.name = dictionary[PetKeys.name] as! String object.age = dictionary[PetKeys.age] as! Int object.petType = dictionary[PetKeys.petType] as! String return object}// — — — — End of Pet code — — — — — — — — — //
That’s great! Now we have everything we need to convert an object of type Pet to a dictionary and a dictionary to an instance of Pet. In order to have those methods for a different type, just make that type implement the FirebaseFetchable protocol.
API
Now is time to write the methods to communicate with firebase.
For that, create a protocol named FirabaseCrudable
typealias FirebaseModel = FirebaseFetchable & Makeableprotocol FirebaseCrudable { associatedtype Model: FirebaseModel var ref: DatabaseReference { get } var tableName: String { get } func save(_ model: inout Model, completion: ((Result<Void>) -> Void)?) func fetch(byId id: String, completion: @escaping(Result<Model>) -> Void) func ifNeeded(_ model: Model, completion: @escaping (Result<Model?>) -> Void) func remove(_ model: Model, completion: ((Result<Void>) -> Void)?) func map(model: Model) -> [String: Any]}enum Result<T> { case success(T) case error(Error)}
I also extended the protocol to have a default implementation of those methods:
extension FirebaseCrudable { var ref: DatabaseReference { return Database.database().reference() } var tableName: String { return String(describing: Model.self) } func map(model: Model) -> [String: Any] { return [“\(tableName)/\(model.firebaseId)”: model.toDictionary()] } func save(_ model: inout Model, completion: ((Result<Void>) -> Void)?) { if model.firebaseId == “” { let child = ref.child(tableName).childByAutoId() model.firebaseId = child.key } ref.updateChildValues(map(model: model)) { (error, _) in guard error == nil else { completion?(.error(error!)) return } completion?(.success(())) } } func fetch(byId id: String, completion: @escaping(Result<Model>) -> Void) { ref.child(tableName).child(id).observeSingleEvent(of: .value, with: { (snapshot) in if snapshot.hasChildren(){ let dict = self.convertToDictionary(fromDataSnapshot: snapshot) let model = Model.make(from: dict) completion(.success(model)) } else { completion(.error(ManagerError.notFound(“No value found from \(id) tableName: \(self.tableName)”))) } }) { (error) in completion(.error(error)) } } func ifNeeded(_ model: Model, completion: @escaping (Result<Model?>) -> Void) { var modelRef = model guard !modelRef.isCompleted else { completion(.success(nil)) return } fetch(byId: modelRef.firebaseId) { (result) in switch result { case .error(let error): completion(.error(error)) case .success(let fetchedModel): modelRef.update(other: fetchedModel) completion(.success(modelRef)) } } } func convertToDictionary(fromDataSnapshot dataSnapshot: DataSnapshot?) -> [String: Any] { guard let snapshot = dataSnapshot else{ return [:] } var dictionary = snapshot.value as? [String: Any] ?? [:] dictionary[“firebaseId”] = snapshot.key return dictionary } func remove(_ model: Model, completion: ((Result<Void>) -> Void)?) { ref.child(tableName).child(model.firebaseId).removeValue { (error, _) in if let error = error { completion?(.error(error)) } else { completion?(.success(())) } } }}enum ManagerError: Error{ case notFound(String) var localizedDescription: String { switch self { case .notFound(let message): return message } }}
Our goal with this protocol is to have every type that we want to CRUD with Firebase having a class that will manage the communication of each type with the server. So each type will have its own class that will implement the FirebaseCrudable protocol and it will have all of its extended functionalities.
Now going over the important parts of what is going on here:
Our protocol declares an associatedType Model that is of type FirabaseModel
associatedtype Model: FirebaseModel
FirebaseModel is a typealias that represents two types: FirabaseFetchable and Makeable.
typealias FirebaseModel = FirebaseFetchable & Makeable
We use the associatedType to tell our class what is the type we are working on. We also need to make sure the type has a firebaseId (That’s why the FirebaseFetchable protocol) and it is able to convert to firebase expectations and back (That’s why the Makeable protocol).
In the extension you can go over what is going on but in summary it’s just my implementation of save, remove and fetch methods. If you need help with that you can check the Firebase documentation.
The next step is to create a singleton class that will manage the communication of our Pet class with Firebase.
To do so, we go back to our Sourcery template and add the following lines:
final class {{ type.name }}Manager: FirebaseCrudable { typealias Model = {{ type.name }} static let shared = {{ type.name }}Manager()}
Before the line:
// — — — — End of {{ type.name }} code — — — — — — — — — // comment
This will create our manager class that conforms to FirebaseCrudable protocol.
After building the code we can see a new class named PetManager created in the AutoGenerated file.
Now, if we want to save a new instance of Pet in Firebase, all we need to do is to call the save method from our shared instance of PetManager and that will be it.
let fido = Pet()fido.name = “Fido”PetManager.shared.save(fido)
I you want to have a struct Person and save it in Firebase:
struct Person: FirebaseFetchable { // sourcery: ignore var firebaseId: String // sourcery: ignore var isCompleted: Bool var name: String}
Now you just build, and you can do the following:
let homer = Person()person.name = “Homer”PersonManager.shared.save(homer)
— — — — — — — — — — — — — — — — — — — —
Final Instructions
For the following classes you create here are some precautions:
- Make sure it is implementing FirebaseFetchable
- Every property you declare needs to have a default value. Example:
var name = “”
- Create an empty init
init() {}
This is required because otherwise we would have to create an init method for each class and it would be dificult to be generic on our Sourcery template under those circumstances.
If you check the make(from dictionary [String: Any]) method implementation in the Sourcery template, you can see that we take advantage of the empty init to create an instance of this class and then we do a for loop for each property of the type and assign it to its respective value.
We don’t have this obligation with structs. We don’t need to set a default value for each property as we get the memberwise initializer for free. We also don’t need to create an empty initializer because, in contrast to classes, for structs we can have an initializer inside of an extensions. And that’s what we do in our Sourcery template with the following code:
{% if type|struct %}extension {{ type.name }} { init() { {% for variable in type.variables %} self.{{ variable.name }} = {{ variable.typeName }}() {% endfor %} }}{% endif %}
There is one extra step for both structs and classes that is to comment the FirebaseFetchable variables with
// sourcery: ignore
This is used to ignore those properties when coverting an object to dictionary and back.
To sum up (finally😅)
This may be a lot to digest but the result is pretty cool. Now we can create a new type and simply conform it to FirebaseFetchable. After you build the project you will have an automatically generated class that will have everything you need to save, fetch and remove instances of your type with firebase.
You can check the project source with a app sample from this link.