Search code examples
swiftpredicateswiftdata

Predicate macro doesn't build with Array.contains()


DocumentModel contains tags relationship to TagModel. A View receives selectedTag. Predicate (as part of @Query) should filter documents that have given tag.

let selectedTag = TagModel()

#Predicate<DocumentModel> { document in
   document.tags.contains {
      $0.tagID == selectedTag.tagID
   }
}

Predicate doesn't build because: Cannot convert to closure result type 'any StandardPredicateExpression'.

Predicate macro expands to:

Foundation.Predicate<DocumentModel>({ document in
   PredicateExpressions.build_contains(
       PredicateExpressions.build_KeyPath(
           root: PredicateExpressions.build_Arg(document),
           keyPath: \.tags
       )
   ) {
       PredicateExpressions.build_Equal(
           lhs: PredicateExpressions.build_KeyPath(
               root: PredicateExpressions.build_Arg($0),
               keyPath: \.tagID
           ),
           rhs: PredicateExpressions.build_KeyPath(
               root: PredicateExpressions.build_Arg(selectedTag),
               keyPath: \.tagID
           )
       )
   }
})

Full error message:

Cannot convert value of type 'PredicateExpressions.SequenceContainsWhere<PredicateExpressions.KeyPath<PredicateExpressions.Variable, [TagModel]>, PredicateExpressions.Equal<PredicateExpressions.KeyPath<PredicateExpressions.Variable, String>, PredicateExpressions.KeyPath<PredicateExpressions.Value, String>>>' to closure result type 'any StandardPredicateExpression'

Apple Developer Documentation has example code using contains() and comparing strings:

let messagePredicate = #Predicate<Message> { message in
   message.recipients.contains {
       $0.firstName == message.sender.firstName
   }
}

How to filter documents that contain selected tag?


Solution

  • contains is not the problem here. The problem is selectedTag.tagID.

    You should not access properties of anything else other than document (the parameter of the closure passed to #Predicate). While #Predicate technically supports this (i.e. the macro can expand to some result), SwiftData doesn't. SwiftData can only work with StandardPredicateExpression, which only some PredicateExpressions are.

    Instead of having the closure capture selectedTag, make the closure capture just selectedTag.tagID

    struct SomeView: View {
        let selectedTag: TagModel
        
        @Query var documents: [DocumentModel]
        
        init(selectedTag: TagModel) {
            self.selectedTag = selectedTag
            let selectedTagID = selectedTag.tagID
            _documents = Query(filter: #Predicate<DocumentModel> { document in
                document.tags.contains {
                    $0.tagID == selectedTagID
                }
            })
        }
    
        ...
    }
    

    You can also write selectedTagID = selectedTag.tagID directly in the closure's capture list:

    _documents = Query(filter: #Predicate<DocumentModel> { [selectedTagID = selectedTag.tagID] document in
        document.tags.contains {
            $0.tagID == selectedTagID
        }
    })