Mobile

8 dez, 2025

Um Segredo do # Predicate em Swift: Por Que Uma Linha Muda Tudo?

Publicidade

Um “predicate” é uma condição booleana usada para selecionar ou filtrar registros. Ele responde “este objeto atende ao critério?” e retorna verdadeiro ou falso.

Em bancos/ORMs, o predicate é enviado para o mecanismo de persistência, que retorna apenas os registros que satisfazem a condição.

Já no ecossistema da Apple temos:

  • NSPredicate: API clássica (Core Data, Foundation) baseada em strings formatadas, como `NSPredicate(format: “age >= %d”, 18)` . É flexível, mas menos segura (erros só aparecem em runtime).
  • #Predicate: DSL moderno e tipado (SwiftData/SwiftUI). Você escreve condições com código Swift e o macro gera uma expressão booleana segura em tempo de compilação.

Uso em SwiftData/SwiftUI

@Query(filter: #Predicate<Model> { … }) , o $0 representa um registro do tipo Model . Você compara campos com valores capturados:

// Exemplo com strings: 
#Predicate<CompanyAndLocation> { 
  $0.company.name == companyName && $0.location.location == locationName 
}

// Exemplo por identidade:
#Predicate<CompanyAndLocation> { 
  $0.company.id == companyId && $0.location.id == locationId 
}

Isso permite que o filtro rode no store, retornando apenas os itens que batem com o critério.

Quando usar o #Predicate?

  • Filtrar uma coleção persistida via @Query ou FetchDescriptor para trazer do banco somente o que importa.
  • Substitui NSPredicate quando você quer segurança de tipos e integração direta com SwiftData.

Quais são as vantagens do #Predicate?

  • Tipado e checado em compile-time.
  • Integra-se ao @Query e FetchDescriptor , reduzindo erros de formatação.
  • Melhora completions e refatorações (se você renomear um campo, o compilador ajuda).

Cuidado com algumas pequenas armadilhas!?

  • Comparar “key path com key path” em vez de “campo com valor” causa erros de tipo no DSL.
  • Referenciar self ou propriedades de @Model dentro do bloco sem capturar valores simples pode quebrar a expansão do macro.
  • Comparar relacionamentos por objeto inteiro pode falhar dependendo da versão do SDK; comparar por id ou por atributos únicos é mais robusto.

Show me the code:

// PredicateApp.swift

import SwiftUI
import SwiftData

@main
struct PredicateApp: App {
    var body: some Scene {
        WindowGroup {
            DataView(
                company: Company(name: "Company 1", type: 1),
                location: Location(location: "Location 1", value: 100)
            )
        }
    }
}
// CompanyAndLocation.swift

import SwiftData
@Model
class Company {
  #Unique<Company>([\.name])
  var name: String
  var type: Int

  init(name: String, type: Int) {
    self.name = name
    self.type = type
  }
}

@Model
class Location {
  #Unique<Location>([\.location])
  var location: String
  var value: Int
    
  init(location: String, value: Int) {
    self.location = location
    self.value = value
  }
}

@Model
class CompanyAndLocation {
  #Unique<CompanyAndLocation>([\.company, \.location])
  var company: Company
  var location: Location
    
  init(company: Company, location: Location) {
    self.company = company
    self.location = location
  }
}
// DataView.swift

import SwiftUI
import SwiftData

struct DataView: View {
    
    @Environment(\.modelContext) var modelContext
    var company: Company
    var location: Location
    @Query var companyAndLocation: [CompanyAndLocation]
    
    var body: some View {
        Text("Hello World!")
    }
    
    init(company: Company, location: Location) {
        self.company = company
        self.location = location
        
        let predicate = #Predicate<CompanyAndLocation>
        {
            $0.company == self.company && $0.location == self.location
        }
        
        _companyAndLocation = Query(filter: predicate)
    }
}

Crie um projeto SwiftUI, adicione estes 3 arquivos e compile o projeto. E se você este erro significa que está tudo certo (em receber o erro).

Cannot convert value of type 'PredicateExpressions.Conjunction<PredicateExpressions.
Equal<PredicateExpressions.KeyPath<PredicateExpressions.Variable<CompanyAndLocation>, Company>, 
PredicateExpressions.KeyPath<PredicateExpressions.Value<DataView>, Company>>, 
PredicateExpressions.Equal<PredicateExpressions.KeyPath<PredicateExpressions.Variable<CompanyAndLocation>, 
Location>, PredicateExpressions.KeyPath<PredicateExpressions.Value<DataView>, 
Location>>>' to closure result type 'any StandardPredicateExpression<Bool>'

Esse erro indica que estamos tentando comparar objetos complexos diretamente, quando o #Predicate só aceita comparações de tipos escalares (Int, String, UUID, etc.). Também o erro indica que o fechamento de Predicate está retornando um tipo interno do DSL (Conjunction<Equal<…>> ) que não está sendo aceito como any StandardPredicateExpression, normalmente por construção incorreta do predicate (mistura de variável x valor ou uso de key paths em vez de valores).

O problema está nesta parte do código:

let predicate = #Predicate<CompanyAndLocation>
{
  $0.company == self.company && $0.location == self.location
}

Poderíamos resolver assim:

let companyName = company.name
let locationName = location.location
let predicate = #Predicate<CompanyAndLocation> 
{
  $0.company.name == companyName && $0.location.location == locationName
}

Se eu não precisasse comparar o objeto completo. Mas e se eu precisasse?Neste caso para 2 propriedades seria simples, mas se este objeto tivesse mais propriedades? E se esse objeto fosse atualizado com mais propriedades com o decorrer do tempo? Então eu teria que voltar nesse código para atualizá-lo e garantir uma comparação completa.

Podemos tentar assim:

let predicate = #Predicate<CompanyAndLocation> {
  $0.company.id == company.id && $0.location.id == location.id
}

Aqui estamos tentando acessar company.id e location.id diretamente. O macro não consegue rastrear essas propriedades dinâmicas de objetos externos. Ele espera valores simples e escalares que possam ser “capturados” como constantes. Ou seja, não funciona.

A solução: Compare os identificadores, não os objetos:

Essa é uma questão comum com Swift’s #Predicate macro. O problema está em como o macro captura variáveis. O #Predicate funciona através de type-safe query generation, ou seja, precisa traduzir sua expressão Swift para uma query que pode ser executada no banco de dados (CoreData ou SwiftData).

Então resolvemos assim:

let companyId = company.id
let locationId = location.id
let predicate = #Predicate<CompanyAndLocation> 
{
  $0.company.id == companyId && $0.location.id == locationId
}
  • As variáveis companyId e locationId são capturadas pelo closure como valores simples (Equatable)
  • O macro consegue identificá-las como constantes externas e substituí-las corretamente na query

Conclusão:

#Predicate só consegue traduzir para queries de banco de dados expressões que comparam valores escalares (números, strings, UUIDs). Comparações entre objetos complexos não podem ser traduzidas para SQL/query de dados.

Então dentro de um #Predicatecompare os identificadores e não os objetossempre extraia os valores que você quer comparar em variáveis locais simples antes de usá-las no closure. Isso permite que o macro entenda exatamente quais constantes devem ser “compiladas” na query.

If you’ve enjoyed this article, subscribe and get an email as soon as I publish!
Happy Coding!