ViewModifier where Content is Shape, and needs .stroke()

Most of the below code is just setup to ask/demonstrate my question. (And it will run as is on an iPad in the Playgrounds app ... with a “Blank” 5.1 playground ... otherwise, remove import PlaygroundSupport and simply use XCode.).

The bottom most ViewModifier called Cardify is called on a Shape to convert that “content” into a playing Card for a game. I need the content of the Shape to be used twice within my ViewModifier ... once for those cards that require filled colors (either solid, or transparent) and a second time for shapes that require the shape to be outlined only. That second use of content is where things fall apart, as the compiler complains about me trying to stroke() content (which is only a Shape).

Shapes that are outlined only need stroke. So, I’d like have a line in my ViewModifier with:

content.stroke()

Here’s the Code ... look at the last part, and, hopefully, the comments will help with that I am asking for.

How can I call content.stroke() without an error in the ViewModifier called Cardify ?

import PlaygroundSupport

import SwiftUI


// Diamond Shape
struct SetDiamond: Shape {
    func path(in rect: CGRect) -> Path {
        let upperCenter = CGPoint(x: rect.midX, y: rect.minY)
        let lowerCenter = CGPoint(x: rect.midX, y: rect.maxY)
        let midLeft = CGPoint(x: rect.minX, y: rect.midY)
        let midRight = CGPoint(x: rect.maxX, y: rect.midY)
        var p = Path()
        p.move(to:  upperCenter)
        p.addLine(to: midRight)
        p.addLine(to: lowerCenter)
        p.addLine(to: midLeft)
        p.addLine(to: upperCenter)
        return p
    }
}

// Squiggle 
struct SetSquiggle: Shape {
    var topLeftRadius: CGFloat = 0.0 // top-left radius parameter
    var topRightRadius: CGFloat = 150.0 // top-right radius parameter
    var bottomLeftRadius: CGFloat = 150.0 // bottom-left radius parameter
    var bottomRightRadius: CGFloat = 0.0 // bottom-right radius parameter
    
    func path(in rect: CGRect) -> Path {
        let w = rect.width
        let h = rect.height
        // Make sure the radius does not exceed the bounds dimensions
        let tr = min(min(self.topRightRadius, h/2), w/2)
        let tl = min(min(self.topLeftRadius, h/2), w/2)
        let bl = min(min(self.bottomLeftRadius, h/2), w/2)
        let br = min(min(self.bottomRightRadius, h/2), w/2)
        var  p = Path()
        p.move(to: CGPoint(x: w / 2.0, y: 0))
        p.addLine(to: CGPoint(x: w - tr, y: 0))
        p.addArc(center: CGPoint(x: w - tr, y: tr), radius: tr, startAngle: Angle(degrees: -90), endAngle: Angle(degrees: 0), clockwise: false)
        p.addLine(to: CGPoint(x: w, y: h - br))
        p.addArc(center: CGPoint(x: w - br, y: h - br), radius: br, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 90), clockwise: false)
        p.addLine(to: CGPoint(x: bl, y: h))
        p.addArc(center: CGPoint(x: bl, y: h - bl), radius: bl, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 180), clockwise: false)
        p.addLine(to: CGPoint(x: 0, y: tl))
        p.addArc(center: CGPoint(x: tl, y: tl), radius: tl, startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 270), clockwise: false)
        p.addLine(to: CGPoint(x: w / 2.0, y: 0))
        return p
    }
}


// struct to wrap any shape 
struct AnyShape: Shape {
    func path(in rect: CGRect) -> Path {
        return _path(rect)
    }
    init<S: Shape>(_ wrapped: S) {
        _path = { rect in
            let path = wrapped.path(in: rect)
            return path
        }
    }
    private let _path: (CGRect) -> Path
}


// works hand-in-hand with AnyShape struct above
func getShape(_ shape: SetCardShape ) -> some Shape {
    switch shape {
    case .circle:
        return AnyShape( Circle() )
    case .diamond:
        return AnyShape( SetDiamond() )
    case .squiggle:
        return AnyShape( SetSquiggle() )
    }
}


enum SetCardShape {
    case circle, diamond, squiggle
}

struct CardForView: Identifiable {
    var pips: Int
    var shape: SetCardShape
    var color: Color
    var shading: Double
    var isSelected: Bool
    var id: Int
}



struct GameBoard: View {
    
    var body: some View {
        
        ZStack {
            
        
        VStack {
            
            HStack {
                SetCard02(card: CardForView(pips: 1, shape: .circle, color: .red, shading: 1.0, isSelected: false, id: 1))
                SetCard02(card: CardForView(pips: 3, shape: .squiggle, color: .green, shading: 0.35, isSelected: false, id: 2))
                SetCard02(card: CardForView(pips: 2, shape: .squiggle, color: .green, shading: 0.35, isSelected: false, id: 3))
                SetCard02(card: CardForView(pips: 1, shape: .diamond, color: .red, shading: 0.001, isSelected: false, id: 4))
            }
            
            HStack {
                SetCard02(card: CardForView(pips: 2, shape: .diamond, color: .red, shading: 0.001, isSelected: false, id: 5))
                SetCard02(card: CardForView(pips: 2, shape: .circle, color: .purple, shading: 0.001, isSelected: false, id: 6))
                SetCard02(card: CardForView(pips: 1, shape: .squiggle, color: .purple, shading: 0.35, isSelected: false, id: 7))
                SetCard02(card: CardForView(pips: 3, shape: .circle, color: .red, shading: 1.0, isSelected: false, id: 8))
            }
            
            HStack {
                SetCard02(card: CardForView(pips: 3, shape: .circle, color: .green, shading: 1.0, isSelected: false, id: 9))
                SetCard02(card: CardForView(pips: 1, shape: .diamond, color: .purple, shading: 0.35, isSelected: false, id: 10))
                SetCard02(card: CardForView(pips: 3, shape: .squiggle, color: .red, shading: 0.001, isSelected: false, id: 11))
                SetCard02(card: CardForView(pips: 2, shape: .circle, color: .red, shading: 1.0, isSelected: false, id: 12))
            }
            }
            
        }
        
    }
}

// SetCard01 doesn't use a ViewModifier ... 
// SetCard01 is what SetCard02 with Cardify ViewModifier should accomplish
struct SetCard01: View {
    let card: CardForView
    var body: some View {
        ZStack {
            VStack {
                ForEach( 0..<card.pips ) { _ in
                    ZStack {
                        getShape(self.card.shape).opacity(self.card.shading)
                        getShape(self.card.shape).stroke()
                    }
                }
            }
            .foregroundColor(self.card.color)
                .padding() // for shape in RoundedRect
            RoundedRectangle(cornerRadius: 10).stroke(lineWidth: card.isSelected ? 3.0 : 1.0).foregroundColor(.orange)
        }
        .scaleEffect(card.isSelected ? 0.60 : 1.0 )
            .padding() // for spacing between cards 
    }
}



struct SetCard02: View {
    
    let card: CardForView
    
    var body: some View {
        ZStack {
            getShape(card.shape)
                .modifier(Cardify(card: card, shape: getShape(card.shape) as! AnyShape))
        }
        .scaleEffect(card.isSelected ? 0.60 : 1.0 )
            .padding() // for spacing between cards 
    }
}



struct Cardify: ViewModifier {
    let card: CardForView
    let shape: AnyShape
    func body(content: Content)  -> some View {
        ZStack {
            VStack {
                ForEach( 0..<card.pips ) { _ in
                    ZStack {
                        content.opacity(self.card.shading)
                        // content.stroke() // the goal is to uncomment THIS line and avoid the work-around next line, but content (which is really only a shape) can not use .stroke()
                        
                        
                        self.shape.stroke() // THIS line is a work-around; I'd really like previous commented-out line to replace this one.
                                            // If the above commented-out line could work, then I would not need the shape var in this ViewModifier struct for the workaround.  
                                            // Afterall, content is really a Shape, but Swift complains that type Any 
                                            // does not have a member "stroke()"   How can I simply call content.stroke() instead 
                                            // of initing Cardify with an AnyShape just to be able to call .stroke() for those cards 
                                            // with shapes that are not filled ???? 
                    }
                    .foregroundColor(self.card.color)
                }
            }
                .padding() // for shape in RoundedRect
            RoundedRectangle(cornerRadius: 10).stroke(lineWidth: card.isSelected ? 3.0 : 1.0).foregroundColor(.orange)
        }
        .aspectRatio(2/3, contentMode: .fit)
    }
}


PlaygroundPage.current.setLiveView( GameBoard() )

If Cardify doesn't need any feature from ViewModifier, like Animatable, having @State, etc., it'd be easier to have it as a function in an extension to Shape protocol. All the more so that it only applies to Shape, not View in general.

extension Shape {
  func cardify(using card: CardForView) -> some View {
    VStack {
      ForEach( 0..<card.pips ) { _ in
        ZStack {
          self.opacity(card.shading)
          self.stroke()
        }
      }
    }
    .padding() // for shape in RoundedRect
    .aspectRatio(2/3, contentMode: .fit)
    .foregroundColor(card.color)
    .background(
      RoundedRectangle(cornerRadius: 10)
        .stroke(lineWidth: card.isSelected ? 3.0 : 1.0)
        .foregroundColor(.orange)
    )
  }
}

You can also make SetCardShape itself Shape. I feels like that's how one should type-erase Shape.

enum SetCardShape: Shape {
  case circle, diamond, squiggle

  func path(in rect: CGRect) -> Path {
    switch self {
    case .circle: return Circle().path(in: rect)
    case .diamond: return SetDiamond().path(in: rect)
    case .squiggle: return SetSquiggle().path(in: rect)
    }
  }
}

or make a new SetCardShapeShape (not gonna bother bikeshedding), if you want to separate View from Model.

PS
This question is solely about SiwftUI, which is Apple's private frame work. I'd suggest that you ask over at Apple Developer Forums instead.