I am trying to provide a custom shape style to have a unified look across my app. The problem is that my shape style depends on the resulting frame of the view:
extension ShapeStyle where Self == RadialGradient {
public static var special: RadialGradient {
RadialGradient(
colors: [Color(.specialStart), Color(.specialEnd)],
center: .init(x: 0.25, y: 0.25),
startRadius: frame.width * 0.35,
endRadius: frame.height * 0.85
)
}
}
Unfortunately only the center
property is a unit point whereas startRadius
and endRadius
are CGFloat
expecting absolute values. As far as I can see there is no api with unit points only. Is there a way to create a custom shape style which depends on the frame?
If you create your own ShapeStyle
, you have access to the EnvironmentValues
. It's not hard to get the size of a view using onGeometryChange
and injecting it into the environment.
struct ContentView: View {
var body: some View {
Rectangle()
.fill(MyShapeStyle())
.modifier(SizeReader())
}
}
struct MyShapeStyle: ShapeStyle {
func resolve(in environment: EnvironmentValues) -> some ShapeStyle {
RadialGradient(
colors: [.blue, .red],
center: .init(x: 0.25, y: 0.25),
startRadius: environment.boundSize.width * 0.35,
endRadius: environment.boundSize.height * 0.85
)
}
}
struct SizeReader: ViewModifier {
@State var size: CGSize = .zero
func body(content: Content) -> some View {
content
.environment(\.boundSize, size)
.onGeometryChange(for: CGSize.self) {
$0.size
} action: { newValue in
size = newValue
}
}
}
extension EnvironmentValues {
@Entry var boundSize: CGSize = .zero
}
The size of the view is not necessarily the size of the frame to which the shape style is applied (though most of the time it is), so this is not always 100% accurate. You'd need to handle these cases on a case-by-case basis.
Also note that environment.boundSize.width * 0.35
could be greater than environment.boundSize.height * 0.85
, in which case the two colors would instantaneously switch places.