Macros
You may not be able to attach macro to the wrapper, but you can attach the macro to the protocol
. This is how swift-spyable by @Matejkob works. You write:
@Spyable
public protocol ServiceProtocol {
var name: String { get }
func fetchConfig(arg: UInt8) async throws -> [String: String]
}
It generates:
public class ServiceProtocolSpy: ServiceProtocol {
public var name: String {
get { underlyingName }
set { underlyingName = newValue }
}
public var underlyingName: (String)!
// And so on…
}
So, technically what you are asking is possible, and you even have a pretty big code base to inspire you. That said macros would not be my 1st choice for this kind of functionality. Enter the…
Sourcery
(I'm not affiliated with @krzysztofzablocki, I just really like this repo.)
Macros have tons of problems with compilation times and they involve "virtual" code - code that you can't debug or add to source control.
Sourcery lets you generate the code and store it in a file, just like any other code. No compilation time cost, it is just a normal Swift code.
I will use the spy/mock example here, so you can compare it with swift-spyable from the above. You write:
// sourcery: generate-fake
protocol FileSystem: Sendable {
/// Current working directory.
var cwd: String { get }
}
This is processed by the Sourcery using your template:
@testable import YourModule
{% for type in types.protocols where type|annotated:"generate-fake" %}
class {{ type.name }}Fake: {{ type.name }}, @unchecked Sendable {
{%- for variable in type.allVariables|!definedInExtension %}
// Do some stuff for every property.
{% endfor %}
{%- for method in type.allMethods|!definedInExtension %}
// Do some stuff for every method.
{% endfor %}
}
{% endfor %}
This will generate the following file (that you can check into your source control!):
class FileSystemFake: FileSystem, @unchecked Sendable {
/// Always return this value.
var cwdResult: String?
/// Return specified values in succession.
var cwdResults = [String]()
private var cwdResultsIndex = 0
/// How many times was this property called?
var cwdCallCount = 0
var cwd: String {
self.cwdCallCount += 1
if let r = self.cwdResult { return r }
if self.cwdResultsIndex < self.cwdResults.count {
let r = self.cwdResults[self.cwdResultsIndex]
self.cwdResultsIndex += 1
return r
}
fatalError("FileSystemFake.cwd - missing mock")
}
}
If you are an iOS dev then I highly recommend looking at SwiftGen to automate assets/colors/fonts/localization. Very similar to Sourcery.
Other
One minor thing is that when writing struct Wrapper: MyProtocol
you need to store an inner: MyProtocol
instance. You can store it as any MyProtocol
, but that may involve an existential container (TypeLayout -> existential-container-layout).
In theory it may be better to make the wrapper generic (struct Wrapper<Inner: MyProtocol> { let inner: Inner }
). Then you would have to use some MyProtocol
. This is basically a way of saying: "I'm too lazy so spell the whole type, but the compiler knows it".
The big downside is that you will always need some
concrete type, which can get a bit annoying, and the performance gains would be miniscule (unless you are writing the next SwiftUI that needs to be refreshed at 120Hz).