1

new to swiftui and firestore and struggling with calling this document array. Hoping someone can help me out with the code to call the "items" array into a VStack.

enter image description here

Added code I've been trying

View Model

import SwiftUI
import FirebaseFirestore

class MovieItemViewModel: ObservableObject {
    
    @Published var movieItems = [MovieItem]()
    
    private var db = Firestore.firestore()
    
    func fetchData() {
        db.collection("movies").order(by: "items").addSnapshotListener { (querySnapshot, error) in
            guard let documents = querySnapshot?.documents else {
                print("No Documents")
                return
            }
            
                self.movieItems = documents.map { (queryDocumentSnapshot) -> MovieItem in
                let data = queryDocumentSnapshot.data()
                
                let items = data["items"] as? String ?? ""
                    
                return MovieItem(item: items)
                
            }
            
        }
    }
}

Model

import SwiftUI

struct MovieItem: Identifiable {
    var id: String = UUID().uuidString
    var item: String
}

Main View

struct MovieDetailListView: View {
    
    @ObservedObject private var viewModel = MovieItemViewModel()

    var body: some View {

     VStack(alignment: .leading, spacing: 15){
        ForEach(viewModel.movieItems.indices, id: \.self) { i in
        Text(viewModel.movieItems[i].item) 
        } 
     }
     .onAppear() { self.viewModel.fetchData() }
}
}

6
  • post the code that you did try Commented Dec 15, 2020 at 19:40
  • @Cod3rMax I've added the code I've been messing around with, based on YT vids and answers I've found online. Commented Dec 15, 2020 at 19:54
  • In its current state, on the ForEach text im getting Value of type 'MovieItem' has no member 'name', and on the return in the viewmodel i get Argument passed to call that takes no arguments Commented Dec 15, 2020 at 20:34
  • Please check your property names! 1. Your viewModel MovieItemViewModel doesn't have a property items so you can't do ForEach(viewModel.items.…. You called it movieItems, so ForEach(viewModel.movieItems.…? 2. Your MovieItem doesn't have a property name, so you can't do Text(viewModel.items[i].name). Commented Dec 15, 2020 at 20:53
  • @grg thanks for calling those out. I've made some edits (i've edited the code above). I'm not getting any errors anymore, but its still not calling any data, I've tried print(self.viewModel.fetchData()) in the onAppear but the console is just printing "()" Commented Dec 15, 2020 at 21:24

3 Answers 3

2

There are few issues in the code. Let me address the Firebase issue first

This

.collection("movies").order(by: "items")

is not correct because items is not a document child of movies. However 590706 is. So if the goal is to read the items within 590706, you need to specify that path.

Additionally, .addSnapshotListener leaves an observer/listener on that path and if there are any changes, an event will fire and deliver those changes to your app. It doesn't seem like you want that (yet) - it seems like you just want to read the data on demand (as the user selects the movie for example)

Here's the code that will read the data you're asking about. I omitted any error checking for brevity

func readMovieItems() {
    let movieNum = "590706"
    let refToRead = self.db.collection("movies").document(movieNum)
    refToRead.getDocument(completion: { documentSnapshot, error in
        if let err = error {
             print(err.localizedDescription)
             return
         }

        if let doc = documentSnapshot {
            let title = doc.get("movie") as! String
            let items = doc.get("items") as! [String]
            print(title)
            for item in items {
                print(item)
            }
        }
    })
}

from there, you have a lot of options; suppose this is a Master->Detail setup with the list of movies on the master and when a movie is tapped the details are shown on a detail view. Something like this will do it

struct MasterView: View {
    @Binding var movies: [MovieClass]

    var body: some View {
        List {
            ForEach(movies, id: \.self) { movie in
                NavigationLink(
                    destination: DetailView(selectedMovie: movie)
                ) {
                    Text("\(movie.title)")
                }
            }.onDelete { indices in
                indices.forEach { self.movies.remove(at: $0) }
            }
        }
    }
}
Sign up to request clarification or add additional context in comments.

5 Comments

This is exactly what I'm looking for! Thank you so much! It's printing exactly what I wanted. Now a stupid question (I apologize, I'm new to this). How would I structure the ForEach loop in the view to actually show these results on screen?
@SeanForReal Sorry - got sidetracked. I completed the answer - take a look.
Thanks for much for this Jay. I've already gotten the Master/Detail setup working, I just need to know how to display the feed of items on the detail page. I'm not sure your MasterView does this, it seems to be more the master page listing the movies with links to the detail page. On the detail page, how would you display the list of items?
@SeanForReal As you mentioned in your comment the master displays a 'list of movies' which has links to the detail page - your question is really asking how to read and display a list and that's a template. The 'list of items' works in exactly the same fashion - pass the list to the detailView so it's contained in a var @Binding var someItems: [YourItems] and use the same code, minus the link obviously, to display that list.
Thanks so much for your help. I understand what you mean, not 100% sure how to do it but I'm going to work on it and try to figure it out. I'm very new to this. I've accepted your answer, thanks so much!
1

I think you want to have your movieItems property to contain an array of movie item arrays, not an array of all items from all docs right? If this is incorrect let me know and I can change the answer.

var movieItems = [[MovieItem]]()

func fetchData() {
    
    db.collection("movies").order(by: "items").addSnapshotListener { snapshot, error in
        if error != nil {
            return
        } else {
            let documents = snapshot!.documents
            
            for eachDoc in documents {
                let data = eachDoc.data()
                let items = data["items"] as! [String]
                let itemArray = generateMovieItemsWith(items: items)
                self.movieItems.append(itemArray)
            }
        }
    }
    
}

func generateMovieItemsWith(items: [String]) -> [MovieItem] {
    var movieItems = [MovieItem]()
    for each in items {
        let movieItem = MovieItem(item: each)
        movieItems.append(movieItem)
    }
    return movieItems
}

struct MovieItem: Identifiable {
    var id: String = UUID().uuidString
    var item: String
    
    init(item: String) {
        self.item = item
    }
}

1 Comment

Ah ok, we're close. I see what this is doing. It's collecting all of the "items" for all of the movies and displaying them in the list, so no matter what movie I'm on, its showing one big list of items. What I'm hoping is that is only shows the items for the selected movie.
0

Add this extension:

extension Decodable {
init(fromDictionary: Any) throws {
let data = try JSONSerialization.data(withJSONObject: fromDictionary, options: JSONSerialization.WritingOptions.prettyPrinted)
let decoder = JSONDecoder()
self = try decoder.decode(Self.self, from: data)
      }
}

And ensure your Model conforms to decodable.

Then simply call:

  db.collection("movies").order(by: "items").addSnapshotListener { (querySnapshot, error) in
    if let error = error {
        print(error.localizedDescription)
        return
    }
    guard let snap = snapshot else {
        print("Error getting data")
        return
    }
    var movies = [MoveItem]()
    for document in snap.documents {
        let dict = document.data()
        guard let decodedMovie = try? MovieModel.init(fromDictionary: dict) else {return}
        movies.append(decodedMovie)
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.