1

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
  • Your handling of DispatchGroup is wrong. inside the loop before calling the asynchronous task call enter and inside the completion handler call leave. And it's crucial sure that calling enter and leave is balanced. Commented Sep 7, 2022 at 13:58
  • Can you provide an example of where to place enter and leave? It needs to be inside the documents.forEach so that every album actually has songs Commented Sep 7, 2022 at 14:11

2 Answers 2

3

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.

Sign up to request clarification or add additional context in comments.

Comments

0

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

6 Comments

Instead of the multiple leave() lines put one defer {group.leave()} statement right before the if let error... line. This ensures that leave is called once wherever the code exits the scope. And the very first enter/leave pair makes no sense.
I need to wait for the self.albums.append until my albumSongs are actually filled so having group.notify at the bottom of the function does not make sense for me
@AdamLeet Don't wait, you get notified. Imagine the group as a counter. enter increases the counter and leave decreases it. If the counter reaches zero the group calls notify.
I am confused whether I can achieve what I want to do with DispatchGroup or not. I just want to wait for my albumsongs array to fill before I create the album, therefore I call enter before the songs are written into the array and leave after it is done. But this leads to the albums having the same songs.
The group gets notified and I create my albums when the songs fetched only once, so the albums will have the same songs.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.