Function that returns any Shape (AnyShape?)

I'm pulling my hair out over here.
I'm just trying to produce a struct that has a member variable that can accept any shape, such as Circle(), Rectangle(), RoundedRectangle(), etc.

of course, the compiler is very mad about this, so I tried to write a function that takes an enum and returns 'some Shape' but that doesn't work because of opaque return types...

I'm thinking I need to build an 'AnyShape' using type erasure but it's super confusing to me. Does anyone know how to do this?

Looking for any helps / pointers you guys have. I've wasted hours on this.

2 Likes

If the underlying type doesn't change for each variable, you need generic struct:

struct Foo<SomeShape: Shape> {
  var shape: SomeShape
  ...
}

Say, if what you want is to store Circle, then change to Rectangle on the same variable, you need type erasure.

I have an AnyShape in one of my projects:

#if canImport(SwiftUI)

import SwiftUI

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public struct AnyShape: Shape {
    public var make: (CGRect, inout Path) -> ()

    public init(_ make: @escaping (CGRect, inout Path) -> ()) {
        self.make = make
    }

    public init<S: Shape>(_ shape: S) {
        self.make = { rect, path in
            path = shape.path(in: rect)
        }
    }

    public func path(in rect: CGRect) -> Path {
        return Path { [make] in make(rect, &$0) }
    }
}

#endif

However, I've only ended up using the initializer that takes a closure, not the initializer that takes a shape.

4 Likes

Type erasure would look like this:

public struct AnyShape: Shape {
    private var base: (CGRect) -> Path
    
    public init<S: Shape>(shape: S) {
        base = shape.path(in:)
    }
    
    public func path(in rect: CGRect) -> Path {
        base(rect)
    }
}

Though I ask that you consider this carefully if you need type-erasure. In SwiftUI, chances are, you can do away with other mechanisms (like generic, if/else block).

3 Likes

Wow. Thank you so much for the help guys! I always feel weird stealing code but @mayoff your code really did work like a charm. Looks like it comes down to just passing along the path?

Thanks again guys

1 Like

You can use my AnyShape by providing a shape:

let aShape: AnyShape = AnyShape(Rectangle())

Or you can pass a closure that draws the path:

let aShape: AnyShape = AnyShape { rect, path in
    path.addEllipse(in: rect)
}

Just to note, you don't need to capture make since the closure doesn't escape, not that it makes any difference.

I’m struggling with something kind of similar. Anyone know how to let a ViewModifier know that it’s `contents is something that conforms to the Shape protocol ? I need a Shape stroked for a card game app ... and compiler complains that stroke() is not a member of Any view.

i use your AnyShape, works great.

just as you noted to use it as a last resort only, is it possible to get rid of this type erasure in the following fragment without introducing code duplication?

    let shape = isSquare ?
        AnyShape(RoundedRectangle(cornerRadius: size*0.3 + delta).inset(by: delta + extra)) :
        AnyShape(Circle().inset(by: delta + extra))
    
    return Group {
        if icon != nil {
            Image(uiImage: icon!)
                .resizable()
                .aspectRatio(contentMode: .fill)
                .background(Color.yellow)
                .padding(delta)
                .frame(width: w, height: w)
                .clipShape(shape)
        } else {
            Text("XXX")
                .frame(width: w, height: w)
                .font(Font.custom("Courier", size: size*0.4))
                .background(Color.yellow)
                .overlay(shape.stroke(lineWidth: bw).foregroundColor(Color.red))
        }
    }

One thing I would do, is to make Circle just a RoundedRectangle with infinite cornerRadius, or better yet, cornerRadius of width/2.

Swapping Shape like how you did it does lose the ability to animate shape transitions. It is probably fine otherwise to type-erase it since I just realise Shape does not participate in ViewBuilder, only View (which Shape conforms to).

Though I'd personally prefer to make it a proper shape

enum ImportantShape: Shape {
  case square(Float), circle

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

but that could just be me being paranoid about performance impact from type erasure, which used to be quite a problem in the past (not sure about now).

3 Likes