This repository contains some templates (in the Templates/
directory) to use for Code Generation in Swift with Sourcery.
You can see them in action by opening the TemplatesDemo.xcodeproj
Xcode project: its UnitTests
target contains some use case examples for each template, and some code using the generated code in each associated XCTestCase
.
- Type Erasure
- AutoInterface: Generate protocols matching an API
- AutoPropertiesProtocol: Ideal to use Protocol Composition for Dependency Injection
- AutoCaseName: To be able to compare enums with associated values regardless of their payloads
- Other templates: link to places where you can find other templates
This template provides type erasure to any protocol you annotated accordingly.
- Annotate the protocol to type-erase using
// sourcery: TypeErase = X
whereX
is theassociatedtype
to erase - Use the template in
Templates/TypeErase.stencil
with Sourcery to generate Type-Erased code for this annotated protocol(s) - Add the generated
TypeErase.generated.swift
file to your Xcode project - Profit!
You can find some examples in the UnitTests/TypeErasure
directory in this repo, like those:
// sourcery: TypeErase = PokemonType
protocol Pokemon {
associatedtype PokemonType
func attack(move: PokemonType)
}
// sourcery: TypeErase = Model
protocol Row {
associatedtype Model
var sizeLabelText: String { get set }
func configure(model: Model)
}
You can look at the file generated by Sourcery from those examples using the provided Templates/TypeErase.stencil
template. Magic! 🎩✨
This is a first draft of the template, so please don't hesitate to improve it.
I haven't tested a lot of real-world use cases yet so some corner cases are probably missing but feel free to submit a PR to fix them!
The template generates the same code structure as the one explained in this BigNerdRanch article that inspired me to come up with a working template.
You can also find more information about Type-Erasure by watching Gwendolyn's talk at Try! Swift — from which I borrowed the Pokemon examples to test that template.
This template allows to auto-generate a protocol matching the API of a class
/struct
.
- Create an empty phantom protocol
protocol AutoInterface {}
somewhere in your code. - Make your classes or structs conform to
AutoInterface
to opt-in - Optionally, you can customize the name of the generated
protocol
for each type using annotations:- By default, the generated protocol as the same name as the type it was generated from, but prefixed with an
I
(e.g.protocol IWebService
forclass WebService: AutoInterface
- You can use
// sourcery: AutoInterfacePrefix = …
to change that prefix - You can use
// sourcery: AutoInterfaceSuffix = …
to add a suffix - You can use
// sourcery: AutoInterface = …
to force an exact name (in that case theAutoInterfacePrefix
&AutoInterfaceSuffix
are ignored if present)
- By default, the generated protocol as the same name as the type it was generated from, but prefixed with an
- Use the template in
Templates/AutoInterface.stencil
with Sourcery to generate aprotocol
for each opted-in type and automatically make your type conform to that new protocol - Add the generated
AutoInterface.generated.swift
file to your Xcode project - Profit!
You can find some examples in the UnitTests/AutoInterface
directory in this repo, like this:
class UserWSClient: AutoInterface {
struct User {
let id: Int
let name: String
}
let baseURL: URL = URL(string: "https://example.com/api")!
func fetchUsers() -> [User] {
return (0..<10).map { User(id: $0, name: "User\($0)") }
}
func fetchUser(id: Int) -> User? {
return User(id: id, name: "User\(id)")
}
}
Will generate a protocol IUserWSClient
containing all the functions and properties of your type and make your type conform to that protocol:
protocol IUserWSClient {
var baseURL: URL { get }
func fetchUsers() -> [WSClient.UserWSClient.User]
func fetchUser(id: Int) -> WSClient.UserWSClient.User?
}
extension UserWSClient: IUserWSClient {}
You can look at the file generated by Sourcery from this example using the provided Templates/AutoInterface.stencil
template. Magic! 🎩✨
This template allows you to generate one protocol for each property of your type, each protocol exposing that single property.
This allows you to later leverage protocol composition to express exactly which properties of that type you're gonna use and want to be able to access.
This pattern is typically useful to leverage protocol composition for Dependency Injection, which mixes the benefits of having a single struct containing all your dependencies (instead of passing them all one by one in your constructor) and the benefits of compile-type safety + explicit list of dependencies you want without exposing too much
- Declare a phantom protocol
protocol AutoPropertiesProtocol {}
somewhere in your code - Make the class you want to opt-it for this feature to conform to this phantom protocol
- Optionally, you can annotate your properties with the
PropertiesProtocolPrefix
and/orPropertiesProtocolSuffix
annotations to provide a custom prefix/suffix for the name of the generated protocols- By default (if the annotation is not set / has no value), the prefix will be
Has
and there will be no suffix - Reminder: if you want all the properties of your type to have the same prefix/suffix, you can use
// sourcery:begin: PropertiesProtocolPrefix = …
at the beginning of your type +// sourcery:end
at the end to apply the same annotation(s) to all the variables inside that scope.
- By default (if the annotation is not set / has no value), the prefix will be
- Alternatively, you can also optionally annotate your properties with the
PropertiesProtocol
annotation to give an exact name for the protocol to generate for that property. - Finally, use the template in
Templates/AutoPropertiesProtocol.stencil
with Sourcery to generate all the necessary protocols
You can find some examples in the UnitTests/AutoPropertiesProtocol
directory in this repo, like this one:
// sourcery:begin: PropertiesProtocolPrefix = I, PropertiesProtocolSuffix = Container
class Dependencies: AutoPropertiesProtocol {
let webServiceClient: WebServiceClient
let loginManager: LoginManager
let cartManager: CartManager
init(wsClient: WebServiceClient, loginManager: LoginManager, cartManager: CartManager) {
self.webServiceClient = wsClient
self.loginManager = loginManager
self.cartManager = cartManager
}
}
// sourcery:end
With this example above, you can then use Dependencies
as a single struct
containing all the dependencies you need to pass throughout your app workflow, but still keep type safety and explicitness by saying exactly which dependencies of that container you allow to use:
class LoginScreen {
typealias Dependencies = IWebServiceClientContainer & ILoginManagerContainer
private let deps: Dependencies
init(deps: Dependencies) {
self.deps = deps
}
func login() {
print(self.deps.webServiceClient)
print(self.deps.loginManager)
// print(self.deps.cartManager) // Can't access this, which is a good thing :)
}
}
You can look at the file generated by Sourcery from those examples using the provided Templates/AutoPropertiesProtocol.stencil
template. Magic! 🎩✨
This template allows you to generate an extension to expose the root name of cases in enums with associated values.
This especially allows you to test if an enum is of a given case using a simple if
or guard
, without having to do a switch
(which would otherwise be needed if you want to use a guard
clause, because the enum has associated value, and sure you can use if case .loading = state
, but you cannot negate the pattern matching condition)
- Declare a phantom protocol
protocol AutoCaseName {}
somewhere in your code - Make the enums with associated values you want to opt-it for this feature to conform to this phantom protocol
- Finally, use the template in
Templates/AutoCaseName.stencil
with Sourcery to generate all the necessary protocols
You can find some examples in the UnitTests/AutoCaseName
directory in this repo, like this one:
enum State: AutoCaseName {
case notLoaded
case loading(message: String, percent: Double)
case loaded(data: [String])
case error(message: String, code: Int)
}
Without the Sourcery Template, if you wanted to then check if a State
was not in state loaded
without caring about the associated value of that state, you'd still have to use a switch
:
let state = State.loading(message: "In progress…", percent: 0.42)
let isLoading: Bool = {
switch state {
case .loading: return true
default: return false
}
}()
guard !isLoading else { return }
But instead, using this Sourcery template, you can simply do:
let state = State.loading(message: "In progress…", percent: 0.42)
guard state.caseName != .loading else { return }
Also, as the caseName
property returns a YourType.CaseName
(generated by Sourcery)… which is Comparable
, you can also compare if two instances of your enum are the same case regardless of their payload too:
let state1 = State.loaded(data: ["Foo", Bar"])
let state2 = State.loaded(data: ["Baz"])
let state3 = State.error(message)
guard state1.caseName == satte2.caseName, state1.caseName != state3.caseName else { return }
As a last bonus, the generated YourType.CaseName
is also a enum CaseName: String
(so it's RawRepresentable
using a String
) which allows you to get the name of a case as a String
using state.caseName.rawValue
if needed.
You can look at the file generated by Sourcery from those examples using the provided Templates/AutoCaseName.stencil
template. Magic! 🎩✨
- Sourcery itself has some template examples
- You can also find a template by @Liquidsoul on his own repo to auto-generate code for JSON serialization & deserialization.