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() )