0

I'm trying to make an array of Views so that, ideally, each element of the array can have a different composition of child View elements. I have tried to achieve this by creating an array of [some View]. Apparently the compiler inspects the first element of the the array upon initializing the array and expects all elements have a similar composition of subviews. I'd appreciate any help to get around this problem or an alternative solution.

In the simplest form I have created an array of [some View] with two VStack elements, yet each VStack contains two subviews, a Text and an Image with different layout order.

var myArray : [some View] = [
    VStack{
        Text("foo")
        Image("foo_image")
    },
    VStack{
        Image("bar_image")
        Text("bar")
    }
]

Apparently the compiler infers the type of array as () -> TuppleView<Text,Image> by evaluating the first element and complains that the second element is of type () -> TuppleView<Image,Text> can not be converted to the aforesaid type.

My question is that whether there is a way to hide the detail of the containing Views or wrap it in an opaque object so that I can create an array of Views with different elements and layout arrangements.

Here is a minimal reproducible example

struct WrapperView <Content : View> : View , Identifiable{
    var id: String
    let content : Content
    
    var body: some View{
        VStack{
            Text("Some text")
            content
        }
    }
    
    init(id : String , @ViewBuilder content : ()-> Content){
        self.id = id
        self.content = content()
    }
    
}

struct TheItemList {
    
    static var theItems : [WrapperView< some View>] = [
        WrapperView(id : "frist one" , content: {
            HStack{
                Text("foo")
                Image(systemName: "heart")
            }
        }),
        WrapperView(id : "second one" , content: {
            HStack{
                Image(systemName: "bolt")
                Text("bar")
            }
        })
    ]
}

struct TestView : View {
    
    var body: some View{
        ScrollView{
            ForEach(TheItemList.theItems , id: \.id){item in
                item
            }
        }
    }
}

struct TestView_Previews: PreviewProvider {
    
    static var previews: some View {
        TestView()
    }
}

Obviously everything is fine when the order of Text and Image elements in the second HStack matches the first one's.

reordain
  • 1
  • 2
  • How were you planning to later use this array? Perhaps a `@ViewBuilder` function will help instead of the array. – Vadim Belyaev Jan 20 '23 at 12:21
  • Actually I'm trying to pass these elements as the argument to a ViewBuilder inside another View. – reordain Jan 20 '23 at 12:28
  • It is a bad practice to store views in array. Use a function instead that delivers the appropriate view on runtime. – burnsi Jan 20 '23 at 12:32
  • In the final form such an array is used to hold different views to be embedded in other views. @burnsi could you please elaborate on your proposed solution? – reordain Jan 20 '23 at 12:35
  • There is not enough context here to propose a proper solution. Try to create a [mre]. – burnsi Jan 20 '23 at 12:38
  • This might help you get started with an approach that will work https://stackoverflow.com/questions/72496023/adding-animation-to-tabviews-in-swiftui-when-switching-between-tabs/72816878#72816878 – lorem ipsum Jan 20 '23 at 12:55
  • @burnsi I just added a minimal reporduciable example upon your request, I hope that can help. – reordain Jan 20 '23 at 13:14
  • I think in this case it would be better not to hold an array of `View` but to hold an array of the data that will be going into the views. So that your `ForEach` will take the data and create the views. Not `ForEach` over the views themselves. This is not really the way that SwiftUI would work in something like this. – Fogmeister Jan 20 '23 at 13:30
  • @Fogmeister in the real app I try to embed different number of different of Views, each of which have a different layout which are supposed to be created and inserted to a View, my solution is using a wrapper view which holds elements of some View and create an array of such wrappers. – reordain Jan 20 '23 at 13:40
  • I've got something similar in the app I have. In it we use an array of enums which are different "sections" of the content. `.image` `.text` `.header` `.gallery` etc... each enum then holds the require view data. In the view we then switch on the enum and create the view based on the enum value and content. – Fogmeister Jan 20 '23 at 13:44
  • @Fogmeister in my case the items are widely different and are supposed to be reusable for holding items which serve different purposes, and they are supposed to be left open for holding any possible views. – reordain Jan 20 '23 at 13:53
  • There will be some point in the app where you turn data into this array of views. Don't do that wherever you are doing it. Do it in the view itself. – Fogmeister Jan 20 '23 at 13:54

2 Answers2

2

The way I would approach something like this...

You have an array of all sorts of different data. Swift doesn't do heterogeneous arrays but we can get around this by creating an enum to hold the data...

enum ContentSection: Identifiable {
  case image(imageName: String)
  case text(String)
  case card(imageName: String, title: String)
  // other cases...

  var id: // put an id here
}

Then in the view you give it an array of these and use that to create the views.

struct ContentView: View {
  let sections: [ContentSection] = ...

  var body: some View {
    VStack {
      ForEach(sections) { section in
        switch section {
          case .image(let imageName):
            Image(systemName: imageName)
          case .text(let text):
            Text(text)
          case let .card(imageName, title):
            MyCardView(imageName: imageName, title: title)
          // other cases...
        }
      }
    }
  }
}

This will create all your different views based on the type of the enum and the contents held inside each case.

Fogmeister
  • 76,236
  • 42
  • 207
  • 306
  • 1
    @reordain Please update your question to match the problem you are trying to solve. Views cannot be "widely heterogeneous" they are all views. So there's only so much difference between them. In the app I have we have mixed together images, image carousels, videos, charts, buttons, text, whole collapsible sections, recursive content (i.e. sections that collapse and contain an array of content) etc... So it would be interesting to see what views you cannot display using this method. – Fogmeister Jan 20 '23 at 14:52
  • This solution might be applied to the minimal reproducible problem I gave here, yet I'm trying a reusable solution so that I can leave the final content of each wrapped view open. In my application elements feature widely different layouts. – reordain Jan 20 '23 at 17:29
  • @reordain I’m not sure why you deleted the previous comment and posted it again but my reply stands. I would still be interested to see an example of a view that cannot be displayed like this. Right now your method seems to not work. And from the experience I have in the past, this method does work. But if it doesn’t then I’d def like to see an example where it wouldn’t. – Fogmeister Jan 20 '23 at 21:27
  • you are right, eventually every View can be constructed with your proposed method, yet, as a matter of convenience I was hoping to be able to use arrays of Views in the same capacity that is reflected in my original question. Apparently this is not achievable the way I had in mind. Thank you and I appreciate your time and effort. ps. I don't have enough credit to be able to like or approve solution. I need to collect more credit! – reordain Jan 21 '23 at 00:26
-1

Problem Solved! Apparently wrapping the view items in AnyView can fix the problem, simple as that!

var myArray : [some View] = [
    AnyView(VStack{
        Text("foo")
        Image("foo_image")
    }),
    AnyView(VStack{
        Image("bar_image")
        Text("bar")
    })
]

and this how it fixes the above mentioned example:

struct TheItemList  {
    
    static var theItems : [WrapperView< some View>] = [
        WrapperView(id : "frist one" , content: {
            AnyView( HStack{
                Image(systemName: "heart")
                Text("foo")
            })
        })
        ,
        WrapperView(id : "second one" , content: {
            AnyView(HStack{
                Text("bar")
                Image(systemName: "bolt")
            })
        })
    ]
    
}
reordain
  • 1
  • 2