To ensure type-safety when composing views, we have marker interfaces that simulate inheritance. This enables us to enforce that a parent type (eg. VStack) will only receive compatible children (inheriting from View in this instance).
VStack() {
Button() // OK because Button == IButton (inherits IView)
Label() // OK because Label == ILabel (inherits IView)
TextCell() // NOT OK - TextCell == ICell but ICell doesn't inherit IView
}
This is great, but there is a catch.
Since we have to declare views using structs to avoid killing the performance by instantiating a lot, we can't do inheritance the normal way (structs can't inherit structs in .NET), so the marker interface is used as a generic type like this: Button() will return a WidgetBuilder<'msg, IButton>.
F# doesn't support covariance/contravariance...
This means we can't cast a WidgetBuilder<'msg, IButton> into a WidgetBuilder<'msg, IView>, even though IButton inherits from IView.
This is problematic when we want to return different controls in a same conditional expression.
if model.Value then
Button()
else
Label() /// Compile Error - WidgetBuilder<'msg, ILabel> is not of type WidgetBuilder<'msg, IButton>
The return type of a conditional expression is determined by the first branch -- here the Button, so the F# compiler expects all other branches to return a button as well.
To work around this issue, I'm introducing a concept stolen directly from SwiftUI: AnyView (and the equivalent AnyPage and AnyCell).
This is a helper you can wrap around each conditional branch and it will downcast everything into a same type.
You can even continue adding modifiers to the returned widget.
// Return type for the whole expression is WidgetBuilder<'msg, IView>
let widget =
if model.Value then
AnyView(
Button()
)
else
AnyView(
Label()
)
// Since this is IView, we have access to all IView modifiers
widget.backgroundColor(Color.Red)
To ensure type-safety when composing views, we have marker interfaces that simulate inheritance. This enables us to enforce that a parent type (eg. VStack) will only receive compatible children (inheriting from
View
in this instance).This is great, but there is a catch.
Since we have to declare views using structs to avoid killing the performance by instantiating a lot, we can't do inheritance the normal way (structs can't inherit structs in .NET), so the marker interface is used as a generic type like this:
Button()
will return aWidgetBuilder<'msg, IButton>
.F# doesn't support covariance/contravariance... This means we can't cast a
WidgetBuilder<'msg, IButton>
into aWidgetBuilder<'msg, IView>
, even thoughIButton
inherits fromIView
.This is problematic when we want to return different controls in a same conditional expression.
The return type of a conditional expression is determined by the first branch -- here the Button, so the F# compiler expects all other branches to return a button as well.
To work around this issue, I'm introducing a concept stolen directly from SwiftUI:
AnyView
(and the equivalentAnyPage
andAnyCell
).This is a helper you can wrap around each conditional branch and it will downcast everything into a same type. You can even continue adding modifiers to the returned widget.