2

Goal: To have a single struct to manage the appearance and behavior of a custom View.

Status: I have a simple view model which maintains an array of data representing the view. At run time, I draw these views as follows:

struct MyView: View {
    @ObservedObject var viewModel = MyViewModel()
    
    var body: some View {
        Vstack() {
            ForEach(viewModel.data, id: \.id) { data in 
                switch data.shape {
                    case .circle:
                        MyViewCircle(isHighlighted: data.willHighlight)
                    case .ellipses:
                        MyViewEllipses(isHighlighted: data.willHighlight)
                }
            }
        }
    }
}

struct MyViewCircle: View {
    var isHighlighted: Bool

    var body: some View {
        Circle()
            /// a bunch of modifiers
    }
}

struct MyViewEllipses: View {
    var isHighlighted: Bool

    var body: some View {
        Ellipses()
            ///A bunch of modifiers that mirror exactly MyViewCircle
    }
}

This works fine and I am able to update my viewModel which will update my views accordingly.

My Shapes drawing, I can even tap on the view and update my view model and highlight the corresponding shapes (code not shown for brevity

However, in my case, MyViewCircle or MyViewEllipses will have the exact same appearance and behavior, and the only thing different is it's Shape, also I plan on adding a number of other shapes and even custom drawn shapes.

What I'd like to do I'd like to be able to pass an enum or protocol or something in my ForEach loop that would draw the shape I want and simplify without the need for a long switch statement. Something like this:

struct MyView: View {
    @ObservedObject var viewModel = MyViewModel()

    var body: some View {
        VStack() {
            ForEach(viewModel.data, id: \.id) { data in 
                MyViewProtocol(shape: data.shape, isHighlighted: data.IsHighlighted)
            }
        }
    }
}

And the Parent, Super class, protocol, whatever the proper name for MyViewProtocol would look something like:

struct MyViewProtocol: View {
    var shape: ShapeEnum
    var isHighlighted: Bool

    var body: some View {
        switch shape:
        case .circle:
            Circle()
        case .ellipses:
            Ellipses()
        case .triangle:
            MyTrinagle()
        // etc.....
    }
}

I have been scouring any/all WWDC talks, YouTube tutorials, medium articles, and the closest thing I could find was on SO here: Creating BaseView class in SwiftUI which is useful for creating a set of modifiers of views, however I'm still in a lurch if I want to be able to use a plethora of shapes managed by one parent struct view.

I may also be having problems because perhaps this is a red flag bad design pattern for a declarative language like SwiftUI!

I would be so grateful for any tips or ideas!

Maximilian
  • 1,107
  • 11
  • 20
  • 1
    Does this answer your question? [Alternative to switch statement in SwiftUI ViewBuilder block?](https://stackoverflow.com/questions/56736466/alternative-to-switch-statement-in-swiftui-viewbuilder-block) – pawello2222 Jul 25 '20 at 21:22
  • This is really good thank you @pawello2222 – Maximilian Jul 25 '20 at 21:35
  • 1
    The AnyShape from [How to use same set of modifiers for various shapes](https://stackoverflow.com/a/62605936/12299030) can be useful for your case, I think. – Asperi Jul 26 '20 at 05:52
  • @Asperi this is a REALLY elegant solution, thank you! – Maximilian Jul 26 '20 at 20:33

2 Answers2

1

I don't think you can avoid a switch statement, since you need a mapping between your ShapeEnum case and the actual view, but you can centralize it in one place:

enum ShapeEnum {
   case circle, ellipse, square // etc...
}
extension ShapeEnum: View {
   var body: AnyView {
      switch self {
      case circle:   return AnyView(Circle())
      case ellipse:  return AnyView(Ellipse())
      // ...
      default:       return AnyView(EmptyView())
      }
   }
}

And the usage could be:

@State var shapes: [ShapeEnum] = [.circle, .circle, .ellipse]

var body: some View {
   ForEach(shapes, id: \.self) { shape in
      shape
         .padding()
         .background(Color.red)
   }
}
New Dev
  • 48,427
  • 12
  • 87
  • 129
1

While @New Dev and @Pawello2222 had really good sources/solutions, I think the most elegant solution from @Asprei with this Stack Overflow question is the winner.

Maximilian
  • 1,107
  • 11
  • 20