How to Create a SwiftUI RoundedStar Shape?

Viewed 765

This is a self-answered question which is perfectly acceptable (and even encouraged) on Stack Overflow. The point is to share something useful to others.

SwiftUI has a RoundedRectangle Shape. It would be nice to have a five-pointed star with rounded tips that could be used for filling, clipping, and animation.

This Stack Overflow answer shows how to make a RoundedStar as a custom UIView using UIBezierPath.

How can this code be adapted to SwiftUI as a Shape that can be animated?

enter image description here

1 Answers

Here is the RoundedStar code adapted as an animatable SwiftUI Shape:

// Five-point star with rounded tips
struct RoundedStar: Shape {
    var cornerRadius: CGFloat
    
    var animatableData: CGFloat {
        get { return cornerRadius }
        set { cornerRadius = newValue }
    }
    
    func path(in rect: CGRect) -> Path {
        var path = Path()
        let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
        let r = rect.width / 2
        let rc = cornerRadius
        let rn = r * 0.95 - rc
        
        // start angle at -18 degrees so that it points up
        var cangle = -18.0
        
        for i in 1 ... 5 {
            // compute center point of tip arc
            let cc = CGPoint(x: center.x + rn * CGFloat(cos(Angle(degrees: cangle).radians)), y: center.y + rn * CGFloat(sin(Angle(degrees: cangle).radians)))

            // compute tangent point along tip arc
            let p = CGPoint(x: cc.x + rc * CGFloat(cos(Angle(degrees: cangle - 72).radians)), y: cc.y + rc * CGFloat(sin(Angle(degrees: (cangle - 72)).radians)))

            if i == 1 {
                path.move(to: p)
            } else {
                path.addLine(to: p)
            }

            // add 144 degree arc to draw the corner
            path.addArc(center: cc, radius: rc, startAngle: Angle(degrees: cangle - 72), endAngle: Angle(degrees: cangle + 72), clockwise: false)

            // Move 144 degrees to the next point in the star
            cangle += 144
        }

        return path
    }
}

The code is very similar to the UIBezierPath version except that it uses the new Angle type which provides easy access to both degrees and radians. The code to draw the star rotated was removed because it is easy to add rotation to a SwiftUI shape with the .rotationEffect(angle:) view modifier.


Demonstration:

Here is a demonstration that show the animatable qualities of the cornerRadius setting as well as showing what the various cornerRadius settings look like on a full-screen star.

enter image description here

struct ContentView: View {
    @State private var radius: CGFloat = 0.0
    
    var body: some View {
        ZStack {
            Color.blue.edgesIgnoringSafeArea(.all)
            VStack(spacing: 40) {
                Spacer()
                RoundedStar(cornerRadius: radius)
                    .aspectRatio(1, contentMode: .fit)
                    .foregroundColor(.yellow)
                    .overlay(Text("     cornerRadius: \(Int(self.radius))     ").font(.body))
                HStack {
                    ForEach([0, 10, 20, 40, 80, 200], id: \.self) { value in
                        Button(String(value)) {
                            withAnimation(.easeInOut(duration: 0.3)) {
                                self.radius = CGFloat(value)
                            }
                        }
                        .frame(width: 50, height: 50)
                        .foregroundColor(.black)
                        .background(Color.yellow.cornerRadius(8))
                    }
                }
                Spacer()
            }
        }
    }
}

Running in Swift Playgrounds on iPad

This runs beautifully on an iPad in the Swift Playgrounds app. Just add:

import PlaygroundSupport

at the top and

PlaygroundPage.current.setLiveView(ContentView())

at the end.


Using the RoundedStar shape to create EU Flag

Demo of EU flag running in iPhone 12 simulator

struct ContentView: View {
    static let flagSize: CGFloat = 234 // Change this to resize flag
    
    let flagHeight: CGFloat = flagSize
    let flagWidth: CGFloat = flagSize * 1.5
    let radius: CGFloat = flagSize / 3
    let starWidth: CGFloat = flagSize / 9
    let pantoneReflexBlue = UIColor(red: 0, green: 0x33/0xff, blue: 0x99/0xff, alpha: 1)
    let pantoneYellow = UIColor(red: 1, green: 0xcc/0xff, blue: 0, alpha: 1)
    
    var body: some View {
        ZStack {
            Color(pantoneReflexBlue).frame(width: flagWidth, height: flagHeight, alignment: .center)
            ForEach(0..<12) { n in
                RoundedStar(cornerRadius: 0)
                    .frame(width: starWidth, height: starWidth)
                    .offset(x: radius * cos(CGFloat(n) / CGFloat(12) * 2 * .pi), y: radius * sin(CGFloat(n) / CGFloat(12) * 2 * .pi))
                    .foregroundColor(Color(pantoneYellow))
            }
        }
    }
}
Related