swiftgenericsswiftuiswift-protocolsswiftui-environment

Replicating foregroundStyle(_:) in SwiftUI


In SwiftUI, foregroundStyle(_:) seems to do some @environment magic, but it's also not accessible through any @Environment(\.insertMagicKeyHere) key that I can find, so I have been trying to make my own version of it to apply to some custom views.

However, in the view, when I try to apply the custom view, it gives me an error:

@Environment(\.elementStyle) var elementStyle

var body: some View {
    CustomShape()
        .foregroundStyle(elementStyle) //Type 'any ShapeStyle' cannot conform to 'ShapeStyle'
}

I believe I understand why this is an issue, but I don't know what the magic sauce is to allow my modifier to accept any kind of ShapeStyle. I've tried any number of different generics or whatnot, but I just can't figure out for the life of me how to store an object that conforms to ShapeStyle for use in filling the shape.

How does Apple do it? Is there some kind of magic I don't understand? For reference, my EnvironmentKey is provided below. I would love to not have to write a different modifier/key for every single kind of ShapeStyle. There has to be a way to do this that I'm not seeing, right?

struct ElementStyleKey: EnvironmentKey {
    static var defaultValue: any ShapeStyle = .primary
}

extension EnvironmentValues {
    var elementStyle: any ShapeStyle {
        get { self[ElementStyleKey.self] }
        set { self[ElementStyleKey.self] = newValue }
    }
}

struct ElementStyleModifier: ViewModifier {
    var style: any ShapeStyle
    func body(content: Content) -> some View {
        content
            .environment(\.elementStyle, style)
    }
}

extension View {
    public func elementStyle(_ style: any ShapeStyle) -> some View {
        modifier(ElementStyleModifier(style: style))
    }
}

Solution

  • There is the AnyShapeStyle type eraser that you can use as the type of the environment value.

    struct ElementStyleKey: EnvironmentKey {
        static var defaultValue: AnyShapeStyle = AnyShapeStyle(.primary)
    }
    
    extension EnvironmentValues {
        var elementStyle: AnyShapeStyle {
            get { self[ElementStyleKey.self] }
            set { self[ElementStyleKey.self] = newValue }
        }
    }
    
    struct ElementStyleModifier: ViewModifier {
        var style: AnyShapeStyle
        func body(content: Content) -> some View {
            content
                .environment(\.elementStyle, style)
        }
    }
    
    extension View {
        public func elementStyle<S: ShapeStyle>(_ style: S) -> some View {
            modifier(ElementStyleModifier(style: AnyShapeStyle(style)))
        }
    }