skip to Main Content

I get this Error -> Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0) randomly. I don’t quite understand when exactly it happens. Most of the times it is when the view refreshes. The Error appears at the line where group.leave() gets executed.

What am I trying to do:
I want to fetch albums with their image, name and songs that also have a name and image from my firebase database. I checked for the values and they’re all right as far as I can tell. But when trying to show them it is random what shows. Sometimes everything is right, sometimes one album gets showed twice, sometimes only one album gets showed at all, sometimes one album has the songs of the other album.

My firebase database has albums stored as documents, each document has albumimage/name and 2 subcollections of "unlocked" with documents(user uid) that store "locked":Bool and "songs" with a document for each song that stores image/name
enter image description here

This is the function that fetches my albums with their songs:

    let group = DispatchGroup()
    @State var albums: [Album] = []
    @State var albumSongs: [AlbumSong] = []

func fetchAlbums() {
FirebaseManager.shared.firestore.collection("albums").getDocuments { querySnapshot, error in
            if let error = error {
                print(error.localizedDescription)
                return
            }
            guard let documents = querySnapshot?.documents else {
                return
            }
            let uid = FirebaseManager.shared.auth.currentUser?.uid
            
            documents.forEach {  document in
                let data = document.data()
                let name = data["name"] as? String ?? ""
                let artist = data["artist"] as? String ?? ""
                let releaseDate = data["releaseDate"] as? Date ?? Date()
                let price = data["price"] as? Int ?? 0
                let albumImageUrl = data["albumImageUrl"] as? String ?? ""
                let docID = document.documentID
                FirebaseManager.shared.firestore.collection("albums").document(docID)
.collection("songs").getDocuments { querySnapshot, error in
                    if let error = error {
                        return
                    }
                    guard let documents = querySnapshot?.documents else {
                        return
                    }
                    self.albumSongs = documents.compactMap { document -> AlbumSong? in
                        do {
                            return try document.data(as: AlbumSong.self)
                        } catch {
                            return nil
                        }
                    }
                    group.leave()
                }
                FirebaseManager.shared.firestore.collection("albums").document(docID)
.collection("unlocked").document(uid ?? "").getDocument { docSnapshot, error in
                    if let error = error {
                        return
                    }
                    guard let document = docSnapshot?.data() else {
                        return
                    }
                    group.enter()
                        group.notify(queue: DispatchQueue.global()) {
                            if document["locked"] as! Bool == true {
                        self.albums.append(Album(name: name, artist: artist, 
songs: albumSongs, releaseDate: releaseDate, price: price, albumImageUrl: albumImageUrl))
                                print("albums: ",albums)
                            }
                        }
                }
            }
        }
}

I call my fetchAlbums() in my view .onAppear()

My AlbumSong:

struct AlbumSong: Identifiable, Codable {
    @DocumentID var id: String? = UUID().uuidString
    let title: String
    let duration: TimeInterval
    var image: String
    let artist: String
    let track: String
    }

My Album:

struct Album: Identifiable, Codable {
    @DocumentID var id: String? = UUID().uuidString
    let name: String
    let artist: String
    let songs: [AlbumSong]
    let releaseDate: Date
    let price: Int
    let albumImageUrl: String
  }

I tried looking into how to fetch data from firebase with async function but I couldn’t get my code to work and using dispatchGroup worked fine when I only have one album. I would appreciate answers explaining how this code would work with async, I really tried my best figuring it out by myself a long time. Also I would love to know what exactly is happening with DispatchGroup and why it works fine having one album but not with multiple ones.

2

Answers


  1. Just use them in the correct order:

    let group = DispatchGroup()
    @State var albums: [Album] = []
    @State var albumSongs: [AlbumSong] = []
    
    func fetchAlbums() {
        group.enter()
        FirebaseManager.shared.firestore.collection("albums").getDocuments { querySnapshot, error in
            if let error = error {
                print(error.localizedDescription)
                group.leave()
                return
            }
            guard let documents = querySnapshot?.documents else {
                group.leave()
                return
            }
            
            let uid = FirebaseManager.shared.auth.currentUser?.uid
            
            documents.forEach {  document in
                let data = document.data()
                let name = data["name"] as? String ?? ""
                let artist = data["artist"] as? String ?? ""
                let releaseDate = data["releaseDate"] as? Date ?? Date()
                let price = data["price"] as? Int ?? 0
                let albumImageUrl = data["albumImageUrl"] as? String ?? ""
                let docID = document.documentID
                
                group.enter()
                FirebaseManager.shared.firestore.collection("albums").document(docID)
                    .collection("songs").getDocuments { querySnapshot, error in
                        if let error = error {
                            group.leave()
                            return
                        }
                        guard let documents = querySnapshot?.documents else {
                            group.leave()
                            return
                        }
                        self.albumSongs = documents.compactMap { document -> AlbumSong? in
                            do {
                                group.leave()
                                return try document.data(as: AlbumSong.self)
                            } catch {
                                group.leave()
                                return nil
                            }
                        }
                    }
                
                group.enter()
                FirebaseManager.shared.firestore.collection("albums").document(docID)
                    .collection("unlocked").document(uid ?? "").getDocument { docSnapshot, error in
                        if let error = error {
                            group.leave()
                            return
                        }
                        guard let document = docSnapshot?.data() else {
                            group.leave()
                            return
                        }
                        
                        if document["locked"] as! Bool == true {
                            self.albums.append(Album(name: name, artist: artist,
                                                     songs: albumSongs, releaseDate: releaseDate, price: price, albumImageUrl: albumImageUrl))
                            print("albums: ",albums)
                        }
                        
                        group.leave()
                    }
            }
            
            group.leave()
        }
        
        group.notify(queue: DispatchQueue.global()) {
            // do your stuff
        }
    }
    
    Login or Signup to reply.
  2. I think you are over complicating something that is very simple with async await

    First, your Models need some adjusting, it may be the source of some of your issues.

    import Foundation
    import FirebaseFirestore
    import FirebaseFirestoreSwift
    
    struct AlbumSong: Identifiable, Codable {
        //No need to set a UUID `@DocumentID` provides an ID
        @DocumentID var id: String?
        let title: String
        let duration: TimeInterval
        var image: String
        let artist: String
        let track: String
    }
    struct Album: Identifiable, Codable {
        //No need to set a UUID `@DocumentID` provides an ID
        @DocumentID var id: String?
        let name: String
        let artist: String
        //Change to var and make nil, the initial decoding will be blank
        //If any of the other variables might be optional add the question mark
        var songs: [AlbumSong]?
        let releaseDate: Date
        let price: Int
        let albumImageUrl: String
    }
    

    Then you can create a service that does the heavy lifting with the Firestore.

    struct NestedFirestoreService{
        private let store : Firestore = .firestore()
        
        let ALBUM_PATH = "albums"
        let SONG_PATH = "songs"
        ///Retrieves Albums and Songs
        func retrieveAlbums() async throws -> [Album] {
            //Get the albums
            var albums: [Album] = try await retrieve(path: ALBUM_PATH)
            //Get the songs, **NOTE: leaving the array of songs instead of making a separate collection might work best.
            for (idx, album) in albums.enumerated() {
                if let id = album.id{
                    albums[idx].songs = try await retrieve(path: "(ALBUM_PATH)/(id)/(SONG_PATH)")
                }else{
                    print("(album) :: has invalid id")
                }
            }
            //Add another loop for `unlocked` here just like the one above.
            return albums
        }
        ///retrieves all the documents in the collection at the path
        public func retrieve<FC : Identifiable & Codable>(path: String) async throws -> [FC]{
            let querySnapshot = try await store.collection(path)
                .getDocuments()
            return try querySnapshot.documents.compactMap { document in
                try document.data(as: FC.self)
            }
        }
    }
    

    Then you can implement it with just a few lines in your presentation layer.

    import SwiftUI
    @MainActor
    class AlbumListViewModel: ObservableObject{
        @Published var albums: [Album] = []
        private let svc = NestedFirestoreService()
        
        func loadAlbums() async throws{
            albums = try await svc.retrieveAlbums()
        }
    }
    struct AlbumListView: View {
        @StateObject var vm: AlbumListViewModel = .init()
    
        var body: some View {
            List(vm.albums, id:.id) { album in
                DisclosureGroup(album.name) {
                    ForEach(album.songs ?? [], id:.id){ song in
                        Text(song.title)
                    }
                }
            }.task {
                do{
                    try await vm.loadAlbums()
                }catch{
                    print(error)
                }
            }
        }
    }
    
    struct AlbumListView_Previews: PreviewProvider {
        static var previews: some View {
            AlbumListView()
        }
    }
    

    If you get any decoding errors make the variables optional by adding the question mark to the type like I did with the array.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search