Search code examples
iosswiftswiftdata

SwiftData Filtering on a many to many relationship


I am trying to filter on a many to many relationship. So I have 2 models one is called Movie and the other is called Genre. They look like this.

@Model class Movie {
    @Attribute(.unique) var name: String
    @Relationship(inverse: \Genre.movies) var genres: [Genre]?
    
    init(name: String) {
        self.name = name
    }
}

@Model class Genre {
    @Attribute(.unique) var name: String
    var movies: [Movie]?
    
    init(name: String) {
        self.name = name
    }
}

Now I created a list that has a dynamic filtering option that will allow you to filter on the movie based on the genre. The view code looks like this.

struct MovieList: View {
    @Query var movies: [Movie]
    
    init() {
        _movies = Query(
            filter: #Predicate<Movie> { movie in
//                return movie.genres?.contains { $0.name == "horror" } ?? false
                return true
            }
        )
    }
    var body: some View {
        if !movies.isEmpty {
            List {
                ForEach(movies) { movie in
                    Text(movie.name)
                }
            }
        }
        else {
            ContentUnavailableView("No Movies", systemImage: "heart", description: Text("Add some sample movies by tapping on the button."))
        }
        
    }
}

I came across this article by Paul Hudson for solutions for common errors. The line commented out use to work but is now crashing the app with an error saying this.

error: SQLCore dispatchRequest: exception handling request: <NSSQLFetchRequestContext: 0x600003b10380> , to-many key not allowed here with userInfo of (null)

CoreData: error: SQLCore dispatchRequest: exception handling request: <NSSQLFetchRequestContext: 0x600003b10380> , to-many key not allowed here with userInfo of (null)

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'to-many key not allowed here'

This is how I am adding sample data.

struct ContentView: View {
    @Environment(\.modelContext) var context
    
    var body: some View {
        NavigationStack {
            VStack {
                Button {
                    let movie1 = Movie(name: "Scream \(Int.random(in: 0...1000))")
                    context.insert(movie1)
                    let genre1 = Genre(name: "Horror")
                    movie1.genres = [genre1]
                } label: {
                    Text("Create Dummy Data")
                }
                
                MovieList()
            }
            .navigationTitle("Movies")
        }
    }
}

This is also how I create my container for my App.

import SwiftUI
import SwiftData

@main
struct SwiftDataFilteringApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: Movie.self)
    }
}

Not sure if this was needed but trying to be as detailed as possible. Also would be easy to duplicate by just copying and pasting the code. Now sure if the code was working before I updated Xcode but the thing is it was working before.


Solution

  • Not sure why you've used nullable relationships (it doesn't make sense for a many-to-many relationship to be nil), but if you change them to non-optional ones, then the crash is gone:

    @Model class Movie {
        @Attribute(.unique) var name: String
        @Relationship(inverse: \Genre.movies) var genres: [Genre]
    
        init(name: String, genres: [Genre] = []) {
            self.name = name
            self.genres = genres
        }
    }
    
    @Model class Genre {
        @Attribute(.unique) var name: String
        var movies: [Movie]
    
        init(name: String, movies: [Movie] = []) {
            self.name = name
            self.movies = movies
        }
    }