Sooo. I want to create a macro that I can use together with my resultbuilder. I've got this constraint-build, which is a @resultBuilder:
@resultBuilder
public struct ConstraintBuilder {
public static func buildBlock(_ components: NSLayoutConstraint...) -> [NSLayoutConstraint] {
components
}
...
}
And I would like to create a macro that can be used with this resultbuilder and x number of views.
For example, I would like something like the code below.
#constraint(view1, view2, view3) { arg in
arg.view1.leadingAnchor.constraint(equalTo: arg.view2.trailingAnchor)
}
The important part is that it is failsafe regarding the variables, so for example this is a no-no, since that would work incorrectly if I switched place of view1 and view2
I've tried stuff in the line of this (just playing around):
public struct ConstraintMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
guard let argument = node.argumentList.first?.expression else {
fatalError("compiler bug: the macro does not have any arguments")
}
return """
struct \(context.makeUniqueName("ConstraintBuilderMacroStruct")) {
struct Argument {
let v1: UIView
let v2: UIView
}
let argument: Argument
func constraint(@ConstraintBuilder _ content: (Argument) -> [NSLayoutConstraint]) -> [NSLayoutConstraint] {
content(argument)
}
}
"""
}
}
Is there really any difference between the two forms? In both forms view1 represents the view instance in the first position. Introducing an internal Argument struct doesn't change it.
I understand your requirement. What I meant was that the approach you described doesn't solve the issue, does it? In your both approaches, the only difference between view1, view2, view3 is their position in the argument list. How do you expect compiler to detect they are passed in wrong order?
This is where I was hoping for macro to solve it for me. Because there I can know the name of the arguments, right? And if I can access them via name instead of position, I've solved the issue. Perhaps as named parts of a tuple, or something.
Maybe it wasnt' clear from my example. But I wasn't planning on calling the members of "Argument" v1 v2 etc, I was planning on naming them base on the input variable and create a new "Argument" struct for each case.
No, I don't think that would help. No matter what you call it, it represents the argument in that fixed position. I'd suggest you to implement the macro and see the result.
PS: using meaningful names does help human, not compiler, to detect the error.
Yes, it represents the argument at that fixed position, but if I provide an "arg" I have control over the member of "Argument". If I send it as individual arguments to the closure the caller can name the arguments themselves, which makes it error-prone, that's the difference. Anyway, this discussion doesn't really focus on the issue, the "Argument" solution is just one way of solving the issue.
The real problem I've got is I can't make a macro where I can write:
#constraints(titleLabel, subtitleLabel) { arg in
arg.titleLabel.leadingAnchor.constraint(equalTo: arg.subtitleLabel.trailingAnchor)
}
Because then it's interpreted as the closure is a trailing closure and argument to #constraints while it's supposed perhaps invoke a closure returned by #constraints