Sourcery — How to create templates for iOS projects

Let’s get deep and look at the code that can generate others files

José Neves
5 min readJul 5, 2023
https://unsplash.com/photos/xrVDYZRGdw4

Continuing the first part of this article (link here) where I talk about the usage of Sourcery in iOS projects, let's get deep and look at the code that can generate others files, like: mocks, factories, helpers, etc.

Table of Contents

  1. Create a Static Library just for Sourcery files
  2. Understand .swifttemplate
  3. Coding on .meta.swift template file
  4. Create Core components to reuse code
  5. Create a .mock() method with default values

1. Create a Static Library just for Sourcery files

First thing first, create a new Static Library just for the Sourcery project. With this you can add the SourceryRuntime and SourcerySwift without any impacts in your Main project.

In my example I create a Static Library called SourceryCode and use SPM to add the Packages.

Adding Sourcery packages with SPM

2. Understand the .swifttemplate

When we create a .swifttemplate file we have a simple syntax to follow (doc)

For our example we will use the folling syntax:
- To import swift files <%- includeFile("relative_path_to_file.swift") %>
- To generate output <%= outputMethod() %>

And two Source Annotations:
- To define the file name that will be generated //sourcery:file:fileName.swift
- To mark the end //sourcery:end

Normaly I create a .swifttemplate and a .meta.swift files with the same name, example ModelsMock.swifttemplate:

<%- includeFile("ModelsMock.meta.swift") %>

// sourcery:file:ExampleModels+mock.swift

<%= generateModelsMockOutput() %>

// sourcery:end

Here we have:
- Import related swift file ModelsMock.meta.swift
- Export file name ExampleModels.swift
- Method that will write the contents of the ExampleModels.swift
- End //sourcery:end

3. Coding on .meta.swift template file

The .meta.swift files has the TemplateContext injected, with this we can access some vars to start our job.

3.1. var argument

The first that we can use is argument: [String: NSObject] with this dictionary we can access the args passed on Sourcery.yml

I discribe this configuration file on first article (here)

Lets see an example of this:

Example printing argument var with error

-Sourcery.yml I pass two args, one from local var and another one hardcoded
-ModelsMock.meta.swift I just create a method that will return a string to be written on generated file and print the var argument.

With this, the generated file get a header from sourcery, the print of argument var and the comment that I leave.

You can think "how this code works even with the error on line 12?"
It's simples, the Xcode showing a error because they don't know about the TemplateContext that I wrote earlier, but we have this at run time.

"Has any way to bypass this?" Of course!
We can create a file just to Xcode understant what we have at run time without beaking the Sourcery build.

//  RuntimeHelper.swift

import Foundation
import SourceryRuntime

let argument = [String: NSObject]()
Example printing argument var with error fixed

Now it’s easier to code without errors and with Xcode autocomplete 🥳

3.2. var types

The var types: Types we have access to all the swift files that Sourcery found at sources path.

Example printing types var

And here it's not diferent, we need to add to RuntimeHelper.swift to remove the erros and turn on the autocomplete.

//  RuntimeHelper.swift

import Foundation
import SourceryRuntime

let argument = [String: NSObject]()
let types = Types(types: [])

The var types provides to us a lot of vars filtering the content that Soucery read from source, example:
- struct types.structs: [Struct]
- class types.classes: [Class]
- protocol types.protocols: [Protocol]
- enum types.enum: [Enum]
and the others types can be found at documentation.

For out example let's get all the structs that have the Codable protocol

Example printing structs

With dump we can see that Sourcery found two structs with Codable protocol, knowing this, we can go through each struct and create something with its code.

Code to print the name of the struct and each var with your type

import Foundation
import SourceryRuntime

let autoMockableProtocolName = "Codable"

private let models: [Struct] = {
types.structs.filter {
$0.inheritedTypes.contains(autoMockableProtocolName)
}
}()

func generateModelsMockOutput() -> String {
models.forEach { model in

print(model.name)
model.rawVariables.forEach { variable in
print(variable.name, " -- ", variable.typeName.name)
}
print()
}
return ""
}

In our example we can see the both structs that we have with the vars and types:

Example printing the name of the structs and vars

4. Create Core components to reuse code

Using the option to include files that we see on 2. Understand the .swifttemplate we can create core components to help us.

Let's create a Core.meta.swift to provide to us a var with the project name, and use this to generated a dynamic file name

//  Core.meta.swift
import Foundation
import SourceryRuntime

var projectName: String {
(argument["projectName"] as? String) ?? ""
}

With this we can add the projectName var to ModelsMock.swifttemplate and ModelsMock.meta.swift to generate a SourceryExampleModels+mock.swift for our example.

Note that I import the core file on .swifttemplate

<%- includeFile("../Core/Core.meta.swift") %>
Example using a var declared on core file

5. Create a .mock() method with default values

Finally let’s create a .mock() method for our example

//  Core.meta.swift
import Foundation
import SourceryRuntime

var projectName: String {
(argument["projectName"] as? String) ?? ""
}

func mockParam(variable: Variable) -> String {
switch variable.typeName.name {
case "String":
return "\"\""
case "Int":
return "0"
case "Bool":
return "false"
default:
return ""
}
}

// ModelsMock.meta.swift
let autoMockableProtocolName = "Codable"

private let models: [Struct] = {
types.structs.filter {
$0.inheritedTypes.contains(autoMockableProtocolName)
}
}()


private func generateMock() -> String {
let stringMocks = models.map {
var varNames = [String]()

let paramsLines = $0.rawVariables.map {

varNames.append($0.name)

return "\($0.name): \($0.typeName.name) = \(mockParam(variable: $0))"
}

return """
// MARK: - Generated \($0.name)

extension \($0.name) {

static func mock(
\(paramsLines.joined(separator: ",\n "))
) -> \($0.name) {
.init(
\(varNames.map { "\($0): \($0)" }.joined(separator: ",\n "))
)
}
}
"""
}

return stringMocks.joined(separator: "\n\n")
}

// MARK: - Output

func generateModelsMockOutput() -> String {
"""
//
// \(projectName)Models+mock.swift
// \(projectName)Tests
//
// Created by Sourcery on xx/xx/xx.
//

import Foundation

@testable import \(projectName)

\(generateMock())
"""
}

With the details that you see here, you can create a file for everything in your project!

Just keep in mind that the generated string will be the content of your generated file.

The contents and examples of this article can be found of my GitHub page on branch part2: https://github.com/jnevesjunior/sourceryExample/tree/part2

--

--