Learn Courses My Dashboard

(How) Can I use two different Data Model Structs in one View?

Hi code crew,
2 posts in 2 days? One could say I am pretty active with my learning journey, or pretty stuck in whatever I try to write :slight_smile:

I have a UI with a list of items that the user can tap. This opens a detail view listing all details for one item. However, I want to include 2 values in that detail view that are stored in a different collection in Firestore and that also have their own Data Model struct. The reason for this is that a different app works with that collection and I want to separate “shared” collections from the rest.

I got a function that is pulling these 2 values from Firestore done
This function is passing the values to a struct called CareData in my Data Model done
I think I set up everything correctly in the detail view, but the problem is passing that data from the tabable list to the detail view.

Data Model
Just simple arrays, nothing complex.

struct Items: Decodable, Identifiable {
    var id: String
    var name: String
    …
}

struct CareData {
    var avHeight: Int
    var avWater: Int
}

Detail View

struct Detail: View {
    @EnvironmentObject var model: ViewModel
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>        

    // Passing the model instances
    let item: Items
    let care: CareData

    var body: some View {
        // Some (working) code where data like item.name is shown.
        ...
        
        // The 2 values using the CareData Data Model
        Text("Height: \(care.avHeight)")
        Text("Water: \(care.avWater)")
    }
}

View Model
An important note here: The documents in my shared collection are named after the item name of the not shared collection. When I call the function, I use item.name to query to the correct document in the shared collection.
The function btw only works thanks to Chirs’ help in this thread.

class ViewModel: ObservableObject {
    @Published var itemList = [Items]()
    @Published var careData = [CareData]()

    ...
    func getCareData(item: String) {
        // Some code that gets the data in Firestore from the shared collection and appends it to careData. It is working all well.
    }
}

Problematic List View
Detail() in the NavigationLink is expecting me to pass 2 parameters because I am trying to pass both model instances in the Detail View. I understand to use item: item , as I am looping through all items and need to define what is needed for the Detail View. But what do I need to add for care: ?

struct PlantList: View {
    @EnvironmentObject var model: ViewModel

    var body: some View {
        ForEach(model.itemList) { item in
            NavigationLink(destination: Detail(item: item, care: ?????? )) { // XCode proposing to go for "CareData", but then throws the error "Cannot convert value of type 'CareData.Type' to expected argument type 'CareData'"
                Text(item.name)
            }
            .onAppear {
                model.getCareData(item: item.name)
            }
        }
    }
}

I tried weird things like CareDate.init(avHeight: , avWater:) and it worked when I wrote numbers straight into the code, but I need the variables to be there not some static numbers I came up with.

I hope someone can help. All I want is to show the 2 values in the detail view. This is probably a stupid issue, but I’m frustrated as I seem to not understand the very basics of Swift programming yet.

And because you need to take a little break after trying, failing, and telling the internet your problems, I made this masterpiece of a meme today.

Array! I am using an array that needs to be further defined.
I optimized the code with PlantDetail(plant: plant, care: model.careData[0]), as there is only 1 array in there. However, shortly after launching I am getting the error Fatal error: Index out of range now.
Calling the function with onAppear is too late, as this calls it only after a user clicks an item. I need to figure out how to call this function earlier but within that item loop to get access to item.name :thinking:

No question is stupid. How do you expect yourself to know something you’ve never done?

So you are fetching from Firebase and saving that data to CoreData? Is there a reason for that? Do you need to keep that data in sync when it changes in Firestore, you need to update CoreData??

Off topic, Technically you should name your model as Item cause each element is a single item, not multiple

I’m confused where you put this?
You’re looping through itemList in your ForEach meaning you really only have access to the itemList

First before I give my proposed solution, will itemList and careData, ALWAYS have the exact same number of elements in them?

1 Like

I fetch my data from Firebase and use my Data Model called CareData, not the database CoreData :slight_smile: But now that I see both names side by side it’s easy to misread :slight_smile:

Thank you for the off-topic info regarding naming my model! I’ll fix that, as I completely agree with the reason and I am 100% certain Chris Ching also does it as you say in the courses.

After some additional help on Stackoverflow, I now have it all fixed :slight_smile: So let me present my new List View:

struct PlantList: View {
    @EnvironmentObject var model: ViewModel

    var body: some View {
        ForEach(model.itemList) { item in
            NavigationLink(destination: Detail(item: item, care: model.careData.first ?? CareData(avHeight: 0, avWater: 0))) {
                Text(item.name)
            }
            .onAppear {
                model.getCareData(item: item.name)
            }
        }
    }
}

For care: I had to pass in an array and also define which elements of the array I need. I think that’s where my brain broke, as I know there is just one array (1 avWater and 1 avHeight), but how would Swift know? I now learned I can use .first and that I should provide alternative Ints just in case there is no array :slight_smile:

Ahhh I see :eyes:

Why are you using an array if you only ever need one value?

Also you never answered this question

That are some good questions and after further testing, I realized my code is not working as expected.

Why are you using an array if you only ever need one value?

I am starting to wonder the same thing. I have documents that I can identify throught their document ID as it matches the item.name. All documents only have 1 value for avHeight and 1 value for avWater. Reading your question, I had to look up the definition of an array again in the swift doc. Now I wonder if a dictionary is actually a better fit for this.

First before I give my proposed solution, will itemList and careData , ALWAYS have the exact same number of elements in them?

No :slight_smile: Let’s say this is about trees for the garden.
item list contains documents with unique ids where the fields are name, height, water, fruits etc. It can contain 10 cherry blossom trees, 20 apple trees etc., as they all have their unique id anyway.
careData contains documents where the ID is matching the name of the tree and stores average values taken from item list. So there will only be 1 document called cherry blossom tree with only 2 values: avHeight and avWater.

My thinking for all of this: The user sees a list of different trees, taps on 1 of their cherry blossom trees, sees the details in the Detail view but also gets shown the avHeight and avWater data in that detail view.

When I tested my newest code, I realized that with .onAppear, I am creating an array containing all trees that load in the UI. And out of that array, only the first was selected so I ended up with the same careDate in all the different trees.
This is pretty obvious now, but my thinking was that the function in the for loop will only take the name for the item.name in that for loop and be it’s first array. :dizzy_face: I was so far off and wrong with this thinking.

Can you show what you have now?

Do you have a rough sketch of what you want to be shown? I think I’m getting confused by only seeing the models, and not actually what you want to accomplish

The careData and itemList are both in Firestore, right?

I can’t sadly share something that’s working, but I think this is the better approach.

I worked on my function in ViewModel, as I only need 1 document at the time and it has only 2 values in there. It was previously based on the solution from this thread.

I followed this guide here to write it: Mapping Firestore Data in Swift - The Comprehensive Guide | Peter Friese

Although I watched the dictionary videos here on CWC again, I don’t understand how to declare dataCare right on the 2nd line as a dictionary that uses the struct CareData in my Model. Items is an array. But what does a Dictionary look like? [CareData:CareData]() didn’t work like a bunch of other things I tried in my cluelessness.

@Published var itemList = [Item]()
@Published var careData = [CareData]() // <- Should be a dictionary

func getCareData(item: String) {
        
        let collectionCare = database.collection("collection-name").document(item)
        
        // Get the document
        collectionCare.getDocument { document, error in
            
            if error != nil {
                
                print("Error")
                
            } else {
                
                if let document = document {
                    
                    do {
                        
                        self.careData = try document.data(as: CareData.self)
                        
                    } catch {
                        
                        print(error)
                        
                    }
                }
            }
        }
    }

My Model got a little upgrade following the guide, too.

struct CareData {
    @DocumentID var id: String?
    var avHeight: Int
    var avWater: Int
}


Here a quick draft: Everything green is information coming from itemList. Going back to 10 cherry blossom trees: All of them would show a different date of being planted, could have a different description.
The dark blue parts however are coming from careData and are the same across all cherry blossom trees.
And the same goes for the apple trees: All green parts are individual from apple tree to apple tree, but the blue parts are the same across all apples.

Yes, both are in Firestore and they are both at the first level. One is not the child of the other.

Solved it by following Mapping Firestore Data in Swift - The Comprehensive Guide | Peter Friese again but this time I found their repository on GitHub with examples, which made life so much easier.