25

XCode 15 beta 6.

Just want to do a very simple Query Predicate where the relationship model matches:

@Query(
    filter: #Predicate<Piece> {
        $0.artist == selectedArtist
    },
    sort: [
        SortDescriptor(\Piece.age, order: .reverse)
    ]
) var pieces: [Piece]

and I'm receiving error:

Cannot convert value of type 'PredicateExpressions.Equal<PredicateExpressions.KeyPath<PredicateExpressions.Variable, Artist>, PredicateExpressions.Value>' to closure result type 'any StandardPredicateExpression'

I also tried .id and .persistentModelId on the artist but no luck.

This seems like the most basic predicate use case so I'd be shocked if it's not supported. Any ideas what i'm missing?

My models are basic:

@Model
final class Piece {
    var age: Int
    var artist: Artist
}

And

@Model
final class Artist {
    var name: String
    
    @Relationship(
        deleteRule: .cascade, 
        inverse: \Piece.artist
    )
    var pieces: [Piece]?
}
5
  • 1
    I don’t think you have missed anything, this must be a bug. And you are not alone Commented Sep 4, 2023 at 19:14
  • 1
    Can I ask what the use case is? I recently ran into a similar problem, but solved it by realising that I already had a reference to all the related Piece entries, on Artist.pieces so what's the need for a separate query? In my case, my Pieces items were going to vastly outnumber my Artist items, so filtering every single Piece in order to find relevant ones was not the way to go. I'd be curious to learn more about what you were trying to do? Commented Nov 29, 2023 at 23:49
  • 1
    You bring up a great point. I feel like I was overthinking this. I'm passing this view an "artist" so just displaying "artist.pieces" would have been enough, no need for #query at all. Maybe I thought sorting a lot of pieces at the DB level would be more performant than in array but at that point we're splitting hairs... I appreciate the comment. Commented Nov 30, 2023 at 14:13
  • 1
    That's great to hear. If it makes you feel any better, I went all the way down the rabbit hole on this too. In the end, the mistake I was making was I was inserting the new piece to the modelContext and then setting the relationship (let newPiece = Piece()modelContext.insert(newPiece)piece.artists.append(artist)) — which were instructions I had found elsewhere. INSTEAD the correct approach is somewhat counterintuitive: with SwiftData you can create the object and simply amend the parent and it works: (let newPiece = Piece()piece.artists.append(artist)) Commented Nov 30, 2023 at 16:16
  • @RichardDas For me, the view will not redraw guaranteed when changes happen in the artists array. Manually appending like you described triggers a refresh, but using modelContext.delete(newPiece) would not in my case. Listening to the array via a query would solve this issue. Commented Mar 18, 2024 at 21:47

2 Answers 2

36

You gave me an idea and it worked !!!

Considering that you can't use another model in the predicate, then first set a variable with the persistentModelID and use that variable in the predicate.

I was having the same problem and this worked for me. Of course you need to set the query in your init()

EDIT: I added part of the code that could be helpful.

@Query private var homes: [Home]

init(session: Session) {
    
    let id = session.persistentModelID
    let predicate = #Predicate<Home> { home in
        home.session?.persistentModelID == id
    }
    
    _homes = Query(filter: predicate, sort: [SortDescriptor(\.timestamp)] )
}
Sign up to request clarification or add additional context in comments.

3 Comments

Can you add some code snippets? This is a great suggestion. I didn't know it was possible to manipulate the query at run-time like this.
Done. I copied part of my code (names were modified to protect the innocents, lol)
This worked for me too. How come I have to capture let id = session.persistentModelID but when I just do home.session?.persistentModelID == session.id it does not work?
10

Per answer here, it seems like #Predicate will not let you use another @Model in the code block. Based on this logic, I don't think there's a way to do this as part of @Query without jumping through a bunch of hoops.

If your dataset is relatively small, the best suggestion is to filter it yourself:

@Query(
    sort: [
        SortDescriptor(\Piece.age, order: .reverse)
    ]
) var pieces: [Piece]

private var filteredPieces: [Piece] {
    return pieces.compactMap { piece in
        guard let artist = piece.artist else {
            return nil
        }
        return artist == selectedArtist ? piece : nil
    }
}

and you can use your filtered data below:

var body: some View {
    List {
        ForEach(filteredPieces) { piece in
            //show filtered stuff
        }
    }
}

1 Comment

Thank you! I just spent way too long trying to do item.account.id and item.account.name and it resulted in no returns. Adding accountId and accountName to the model for item fixed this "magically". I had to do this because ultimately there may be 10's or 100's of thousands of items.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.