Home > Software engineering >  Thread 1: EXC_BAD_INSTRUCTION when fetching data
Thread 1: EXC_BAD_INSTRUCTION when fetching data

Time:09-08

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.

CodePudding user response:

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
    }
}

CodePudding user response:

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.

  • Related