Reputation: 13632
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?
Upvotes: 8
Views: 4931
Reputation: 11
You also can create custom modifier
enum ShapeModifierType {
case square
case circle
}
struct ShapeModifier: ViewModifier {
var type: ShapeModifierType
init(_ type: ShapeModifierType) {
self.type = type
}
func body(content: Content) -> some View {
if type == .circle {
content
.clipShape(Circle())
} else {
content
.clipShape(Rectangle())
}
}
}
Usage:
.modifier(ShapeModifier(type == .avatar ? .circle : .square))
Upvotes: 0
Reputation: 257693
Here are possible solutions.
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)
}
}
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))
}
}
Upvotes: 5
Reputation: 186
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.
Upvotes: 5
Reputation: 13632
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)
}
Upvotes: 18