Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI: ViewModifier where content is a Shape

The following code works fine. So really, I’m good ... But I wanted to learn about ViewModifiers ... so my goal is to separate the non-changing stuff from the dynamic stuff to create a .cardify() custom modifier to call on the Shapes Views.

struct SetCard: View {
    
    let pips: Int
    let shape: SetCardShape
    let color: Color
    let shading: Double
    let isSelected: Bool
    
    var body: some View {
        
        ZStack {
            VStack {
                ForEach( 0..<pips ) { _ in
                    ZStack {
                        getShape(self.shape).opacity(self.shading).foregroundColor(self.color)
                        getShape(self.shape).stroke().foregroundColor(self.color)
                    }
                }
            }
            .padding() // for shape in RoundedRect
            RoundedRectangle(cornerRadius: 10).stroke(lineWidth: isSelected ? 3.0 : 1.0).foregroundColor(.orange)
        }
        .scaleEffect(isSelected ? 0.60 : 1.0 )
        .padding() // for spacing between cards 
    }
}

Again, for academic/learning reasons, I wanted to simplify this struct, and use a custom modifier to convert the main content into a standard card.

The below code block only works when I comment out the second content line in the Cardify ViewModifier struct. All cards that used filled shapes for the pips render just fine. Cards that require the Shape to be stroked (i.e. not filled) need that second content in my Cardify ViewModifer to work.

The second content line produces the error:

Value of type Cardify.Content (aka. _ViewModifier_Content) has no member foregroundColor

commenting out .foregroundColor() generates the error: Value of type Cardify.Content (aka. _ViewModifier_Content) has no member stroke

struct SetCardWithCardify: View {
    
    let pips: Int
    let shape: SetCardShape
    let color: Color
    let shading: Double
    let isSelected: Bool
    
    var body: some View {
        ZStack {
            getShape(shape)
            .modifier(Cardify(pips: pips, shape: shape, color: color, shading: shading, isSelected: isSelected))
        }
    .scaleEffect(isSelected ? 0.60 : 1.0 )
        .padding() // for spacing between cards 
    }
}


struct Cardify: ViewModifier {
    
    var pips: Int
    var shape: SetCardShape
    var color: Color
    var shading: Double
    var isSelected: Bool
    
    func body(content: Content)  -> some View {
    
        ZStack {
            VStack {
                ForEach( 0..<pips ) { _ in
                    ZStack {
                        content.opacity(self.shading).foregroundColor(self.color)
                        content.stroke().foregroundColor(self.color)
                    }
                }
            }
            .padding() // for shape in RoundedRect
            RoundedRectangle(cornerRadius: 10).stroke(lineWidth: isSelected ? 3.0 : 1.0).foregroundColor(.orange)
        }
    }
}

Just in case this is important, the following code is the source of getShape() which is the source of content in the Cardify ViewModifier.

func getShape(_ shape: SetCardShape ) -> some Shape {
    switch shape {
    case .circle:
        return AnyShape( Circle() )
    case .diamond:
        return AnyShape( SetDiamond() )
    case .squiggle:
        return AnyShape( SetSquiggle() )
    }
}


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
}

Diamond() and Squiggle() are structs that conform to Shape protocol, properly returning a path from `func path(in rect: CGRect) -> Path in those structs.

I have tried downcasting the second content line with:

(content as! Shape).blah blah blah which generates the error:

Protocol Shape can only be used as a generic constraint because it has Self or associated type requirements.

I have also tried:

(content as! Path)

This doesn't generate any compile-time errors, but when executing it fails with the error:

There was a problem encountered while running, check your code for mistakes around this line. (indicating the second content line).

What can I do to allow the compiler to know the type of content this is so that stroke() and foregroundColor() will work ?

like image 258
RichWalt Avatar asked Jul 01 '20 16:07

RichWalt


1 Answers

I'm not sure that you can, but you can sort of approximate it using an extension on Shape:

extension Shape {
    func modified() -> some View {
        self
            .foregroundColor(Color.secondary.opacity(0.5))
    }
}

// Usage example
struct ContentView: View {
    var body: some View {
        VStack {
            Rectangle().modified()
            Capsule().modified()
        }
    }
}

However, unlike a view modifier you can't access the environment so it's somewhat limited.

like image 139
Wil Gieseler Avatar answered Nov 12 '22 13:11

Wil Gieseler