Il existe un comportement très intéressant avec le protocol Layout de SwiftUI.
Un Layout n’est pas une View. Lorsqu’on crée un Layout, on dimensionne et on place des vues, mais on ne déclare aucun body. Et pourtant, il est tout à fait possible d’utiliser une instance d’un Layout dans la hiérarchie de vue.
struct MyVStack: Layout {
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) -> CGSize { /* ... */ }
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) { /* ... */ }
}
struct MyView: View {
var body: some View {
MyVStack {
Text("1")
Text("2")
}
}
}Si un Layout n’est pas une vue, il doit alors produire une vue qui aura la responsabilité de dimensionner et placer les vues enfants comme décrit dans notre Layout.
// Côté SwiftUI
extension Layout {
func makeView<V: View>(
@ViewBuilder _ content: () -> V
) -> some View { /* ... */ }
}
MyVStack().makeView {
Text("A")
Text("B")
}Seulement, en SwiftUI, nous n’appelons aucune fonction de ce genre. On crée une instance uniquement à partir de notre initializer.
// En SwiftUI, on n'appelle pas une fonction, on crée une instance
MyVStack {
Text("A")
Text("B")
}Attendez… initializer ? Mais d’où vient cette closure dans l’initializer de notre Layout ? SwiftUI regorge d’utilisations ingénieuses des fonctionnalités de Swift et ce cas ne fait pas exception.
Vous n’en avez sûrement pas beaucoup entendu parler lors de son introduction avec Swift 5.2, mais il existe le callAsFunction qui permet d’appeler une instance d’un type (comme une struct ou une class) comme si c’était une fonction, à condition que ce type implémente une méthode nommée callAsFunction.
Comme on l’a dit plus haut, un Layout n’est pas une vue. Il faut donc transformer un Layout en vue. Alors, plutôt que d’appeler la fonction makeView, SwiftUI l’appelle callAsFunction.
extension Layout {
func callAsFunction<V: View>(
@ViewBuilder _ content: () -> V
) -> some View { /* ... */ }
}Ce qui devient de notre côté :
MyVStack().callAsFunction {
Text("A")
Text("B")
}Le callAsFunction permet d’éliminer le boilerplate du nom de la fonction. Swift autorise à synthétiser de cette manière :
// Les parenthèses deviennent optionnelles
MyVStack {
Text("A")
Text("B")
}Ce comportement peut ressembler à première vue à un comportement propre de SwiftUI. Mais il élimine en réalité beaucoup de boilerplate et nous permet de se concentrer sur l’essentiel : dimensionner et placer des vues. Il n’y a aucune magie, juste une excellente utilisation des outils de Swift.