3

As a part of my learning SwiftUI project I do some shape rotations and I have code below. I'm wondering how to avoid of same three lines of modifiers for each shape.

func getShape(shape: Int, i: Int) -> AnyView {
    
    switch shape {
    case 0:
        return AnyView(Rectangle()
                        .stroke(colors[Int(shapeColor)])
                        .frame(width: CGFloat(shapeWidth), height: CGFloat(shapeHeight))
                        .rotationEffect(Angle(degrees: Double(i) * Double(angleStep))))
    case 1:
        return AnyView(Capsule()
                        .stroke(colors[Int(shapeColor)])
                        .frame(width: CGFloat(shapeWidth), height: CGFloat(shapeHeight))
                        .rotationEffect(Angle(degrees: Double(i) * Double(angleStep))))
    case 2:
        return AnyView(Ellipse()
                        .stroke(colors[Int(shapeColor)])
                        .frame(width: CGFloat(shapeWidth), height: CGFloat(shapeHeight))
                        .rotationEffect(Angle(degrees: Double(i) * Double(angleStep))))
    default:
        return AnyView(Rectangle()
                        .stroke(colors[Int(shapeColor)])
                        .frame(width: CGFloat(shapeWidth), height: CGFloat(shapeHeight))
                        .rotationEffect(Angle(degrees: Double(i) * Double(angleStep))))
        
    }
}
Dawy
  • 770
  • 6
  • 23

3 Answers3

6

Using the helper AnyShape type eraser

struct AnyShape: Shape {
    private let builder: (CGRect) -> Path

    init<S: Shape>(_ shape: S) {
        builder = { rect in
            let path = shape.path(in: rect)
            return path
        }
    }

    func path(in rect: CGRect) -> Path {
        return builder(rect)
    }
}

your function can be written as

func getShape(shape: Int, i: Int) -> some View {
    let selectedShape: AnyShape = {
        switch shape {
            case 0:
                return AnyShape(Rectangle())
            case 1:
                return AnyShape(Capsule())
            case 2:
                return AnyShape(Ellipse())
            default:
                return AnyShape(Rectangle())
        }
    }()
    return selectedShape
        .stroke(colors[Int(shapeColor)])
        .frame(width: CGFloat(shapeWidth), height: CGFloat(shapeHeight))
        .rotationEffect(Angle(degrees: Double(i) * Double(angleStep))))
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
3

You can abstract some of the repetition away by using helper functions and extensions.

  1. In the simplified example below I use a @ViewBuilder to clean up the code that we are returning. There is no need to use AnyView and it makes the code much easier to read.

    It would be great if we could return some Shape however this is not currently possible and results in errors. This is why the stroke value has to be repeated for each Shape in the getShape function, otherwise we could have made the extension on Shape instead of View.

  2. I create an extension on View that allows us to combine the modifiers into one function making it more readable and easier to use. Though honestly this part is optional and you could use your two modifiers frame and rotationEffect.

  3. The @ViewBuilder getShape(shape:index:) returns the the shape you have picked with its chosen color, this is then used by the function createShape(shape:index:) where you add the custom modifier that we created as an extension on View.

  4. Finally we create our shape

This should give you a starting point.

struct ShapeView: View {

    @ViewBuilder // 1
    func getShape(shape: Int, i: Int) -> some View {
        switch shape {
        case 0:
            Rectangle().stroke(Color.red)
        case 1:
            Capsule().stroke(Color.red)
        case 2:
            Ellipse().stroke(Color.red)
        default:
            Rectangle().stroke(Color.red)
        }
    }

    func createShape(shape: Int, index: Int) -> some View { // 3
        getShape(shape: shape, i: index)
            .myModifier(width: 200, height: 100, index: index, angleStep: 30)
    }

    var body: some View {
        createShape(shape: 2, index: 1) // 4
    }
}
  
// 2
extension View {
    func myModifier(width: CGFloat, height: CGFloat, index: Int, angleStep: Double) -> some View {
        self
            .frame(width: width, height: height)
            .rotationEffect(Angle(degrees: Double(index) * Double(angleStep)))
    }
}

struct ShapeView_Previews: PreviewProvider {
    static var previews: some View {
        ShapeView()
    }
}

It is such a shame that we cannot return some Shape from the @ViewBuilder or if there existed a @ShapeBuilder as this would mean that we wouldn't have to add the stroke to each shape individually, as a View cannot have a stroke.

Andrew
  • 26,706
  • 9
  • 85
  • 101
0

A nested function can help cleaning up the code:

func getShape(shape: Int, i: Int) -> some View {
    
    func adjustedView<S: Shape>(shape: S) -> some View {
        shape
            .stroke(colors[Int(shapeColor)])
            .frame(width: CGFloat(shapeWidth), height: CGFloat(shapeHeight))
            .rotationEffect(Angle(degrees: Double(i) * Double(angleStep))))
    }
    
    return Group {
        switch shape {
        case 0:
            adjustedView(shape: Rectangle())
        case 1:
            adjustedView(shape: Capsule())
        case 2:
            adjustedView(shape: Ellipse())
        default:
            adjustedView(shape: Rectangle())
        }
    }
}

Another option is to extend Shape with a convenience function. I.e.

extension Shape {
    func adjust(shapeWidth: Double, shapeHeight: Double, angle: Angle) -> some View {
        self.stroke()
            //.stroke(colors[Int(shapeColor)]) // for brevity
            .frame(width: CGFloat(shapeWidth), height: CGFloat(shapeHeight))
            .rotationEffect(angle)
    }
}

It simplifies code a bit. Also there is no need to erase types.

func getShape(shape: Int, i: Int) -> some View {
    Group {
        switch shape {
        case 0:
             Rectangle().adjust(shapeWidth: shapeWidth, shapeHeight: shapeHeight, angle: Angle(degrees: Double(i) * Double(angleStep))))
        case 1:
             Capsule().adjust(shapeWidth: shapeWidth, shapeHeight: shapeHeight, angle: Angle(degrees: Double(i) * Double(angleStep))))
        case 2:
            Ellipse().adjust(shapeWidth: shapeWidth, shapeHeight: shapeHeight, angle: Angle(degrees: Double(i) * Double(angleStep))))
        default:
            Rectangle().adjust(shapeWidth: shapeWidth, shapeHeight: shapeHeight, angle: Angle(degrees: Double(i) * Double(angleStep))))
        }
    }
}
Paul B
  • 3,989
  • 33
  • 46