Get Shape for view dynamically in SwiftUI
Asked Answered
W

3

8

Using Swift 5.2 I would like to create a function to dynamically change the Shape

I have a view like

import SwiftUI

struct CardView: View {
    let suit : Suite
    let rank : Rank
    var body: some View {
        getShape(suite: .heart)
        .fill(Color.red)  // .fill(suit.color)
        .frame(width: 100, height: 100)
     }
}

I would like to create a function with a protocol return type of Shape, I substituted my custom shaps for generic in the example below

func getShape(suite:Suite) -> Shape {
    switch suite {
    case .heart:
        return Circle() // Heart()
    case .diamond:
        return Rectangle() // Diamond()
    case .spade:
        return Circle() // Heart()
    case .club:
        return Circle() // Club()

    }
}

I cannot use an opaque type with some because I am returning different types and I get a compile error

Function declares an opaque return type, but the return statements in its body do not have matching underlying types 

Nor can I leave it as is with the protocol type because I get the error

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

Is there any way I can achieve this elegantly?

Wingspread answered 29/4, 2020 at 13:49 Comment(0)
W
18

By combining @Asperi's answer with

struct AnyShape: Shape {
    init<S: Shape>(_ wrapped: S) {
        _path = { rect in
            let path = wrapped.path(in: rect)
            return path
        }
    }

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

    private let _path: (CGRect) -> Path
}

I can change it to

func getShape(suite:Suite) -> some Shape {
    switch suite {
    case .club:
        return AnyShape(Club())
    case .diamond:
        return AnyShape(Diamond())
    case .heart:
        return AnyShape(Heart())

    case .spade:
        return AnyShape(Spade())
    }
}


struct CardView: View {
    let suit : Suite
    let rank : Rank
    var body: some View {

    getShape(suite: suit)
      .fill(Color.red)
      .frame(width: 100, height: 100)
 }
Wingspread answered 29/4, 2020 at 20:31 Comment(0)
P
5

Here are possible solutions.

Update: Tested with Xcode 13.4 / iOS 15.5

As on now IMO better to place all this model and use ViewBuilder then no any custom wrappers/erasures needed:

enum Suite {
    case heart, diamond, spade, club

    // Generate complete view and return opaque type
    @ViewBuilder
    var shape: some View {    // << here !!
         switch self {
              case .heart:
                    Heart().fill(.red)    // or make it self.color
              case .diamond:
                    Diamond().fill(.red)
              case .spade:
                    Spade().fill(.black)
              case .club:
                    Club().fill(.black)
        }
    }
}


struct CardView: View {
    let suit : Suite
    let rank : Rank

    var body: some View {
        suit.shape          // << as simple as !!
            .frame(width: 100, height: 100)
     }
}

Original: Tested with Xcode 11.4.

struct CardView: View {
    let suit : Suite
    let rank : Rank
    var body: some View {
        // pass all dependencies to generate view
        getShape(suite: .heart, fill: suit.color) 
            .frame(width: 100, height: 100)
     }
}

// Generate complete view and return opaque type
func getShape(suite: Suite, fill color: Color) -> some View {
    switch suite {
        case .heart:
            return AnyView(Heart().fill(color))
        case .diamond:
            return AnyView(Diamond().fill(color))
        case .spade:
            return AnyView(Spade().fill(color))
        case .club:
            return AnyView(Club().fill(color))
   }
}
Palatalized answered 29/4, 2020 at 15:56 Comment(0)
P
5

Just wanted to leave this here:

https://github.com/ohitsdaniel/ShapeBuilder

I recently open-sourced a ShapeBuilder that allows to mark computed properties and functions as @ShapeBuilder or @InsettableShapeBuilder avoiding type-erasure by leveraging Result builders.

This would allow you to write the following code:

import ShapeBuilder

@ShapeBuilder func getShape(suite:Suite) -> some Shape {
  switch suite {
    case .heart:
     Heart()
    case .diamond:
     Diamond()
    case .spade:
     Heart()
    case .club:
     Club()
  }
}

I would also recommend not erasing to AnyView, as stated in the previous answer. Instead, mark you can mark your getShape function with @ViewBuilder. This turns the function body into a view builder, just like the SwiftUI View body property and avoids type-erasure, which allows SwiftUI to maintain your structural view identity more easily.

Potoroo answered 12/8, 2021 at 10:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.