Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get Shape for view dynamically in SwiftUI

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?

like image 331
Ryan Heitner Avatar asked Apr 29 '20 13:04

Ryan Heitner


3 Answers

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)
 }
like image 191
Ryan Heitner Avatar answered Nov 09 '22 13:11

Ryan Heitner


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))
   }
}
like image 22
Asperi Avatar answered Nov 09 '22 13:11

Asperi


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.

like image 3
D. Peter Avatar answered Nov 09 '22 13:11

D. Peter