Search code examples
xcodeswiftui

SwiftUI: Custom Rounded Shapes Like in watchOS 11 Workout Scale


I'm looking to create a new screen in my SwiftUI app that features a "workload rating scale" similar to that of the new training load in watchOS 11 and iOS 18 (https://www.apple.com/watchos/watchos-preview/). It looks super neat and is really intuitive for the user. However, I'm having a lot of trouble creating the custom shapes in code. Trapezoids are being weird, I can't make rectangles with a rounded triangle on top, and I'm running out of patience, haha....

Anyone got any advice on how to make this? Thanks!


watchOS 11 workload rating image

Trying to make custom shapes in SwiftUI that looks like the bars/pills in the images listed, can't get it to work.


Solution

  • If you can compute the points that define the corners of the shape then an easy way to get a rounded shape is to supply the points to RoundedShape. This is a generic wrapper for a set of points that rounds all the corners automatically. It can be found in the answer to Adding rounded corners to custom SwiftUI shape? (it was my answer).

    The basic shape here is a rectangle with a lower top-left corner. Computing the four points for the corners of the shape is therefore quite easy, you just need a way to determine how much shorter the leading side will be. One way is to make the reduction in height a fixed fraction of the width:

    struct SlopedShape: Shape {
        let slopeFraction: CGFloat
        var cornerRadius: CGFloat = 16
    
        func path(in rect: CGRect) -> Path {
    
            // See https://stackoverflow.com/a/78497329/20386264
            RoundedShape(
                points: [
                    CGPoint(x: rect.minX, y: rect.maxY),
                    CGPoint(x: rect.minX, y: rect.minY + rect.width * slopeFraction),
                    CGPoint(x: rect.maxX, y: rect.minY),
                    CGPoint(x: rect.maxX, y: rect.maxY)
                ],
                cornerRadius: cornerRadius
            )
            .path(in: rect)
        }
    }
    

    If you have multiple shapes that are next to each other and you want the slope to look continuous, apply top padding to the leading shapes. The height of the padding should correspond to the slope height of the remaining width (including spacing).

    Example use:

    struct ContentView: View {
        let slopeFraction: CGFloat = 0.4
        let spacing: CGFloat = 8
    
        var body: some View {
            HStack(spacing: spacing) {
                SlopedShape(slopeFraction: slopeFraction)
                    .padding(.top, (50 + spacing) * slopeFraction)
                    .frame(width: 100)
                SlopedShape(slopeFraction: slopeFraction)
                    .frame(width: 50)
            }
            .foregroundStyle(.gray)
            .frame(height: 120)
        }
    }
    

    Screenshot