swiftui

Custom ShapeStyle dependent on resulting frame


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?


Solution

  • 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.