The Setup

I was building Inbox/Archive list views for a document scanning app. Simple enough: filter documents by status using SwiftData’s @Query. Two compile-time surprises were waiting.

Pitfall 1: #Predicate Can’t Compare Enums

What I Wrote

The natural approach:

@Model
class Document {
    var status: DocumentStatus  // enum stored property
    // ...
}

enum DocumentStatus: String, Codable {
    case inbox, archived
}

Then in the view:

@Query(
    filter: #Predicate<Document> { $0.status == .inbox },
    sort: \Document.createdAt,
    order: .reverse
) private var documents: [Document]

Looks clean. Doesn’t compile.

Why It Fails

#Predicate is a Swift macro that translates predicate expressions into something Core Data can execute under the hood. It only supports primitive types for comparisons — String, Int, Bool, and friends. Enum types aren’t in that set.

SwiftData does store enums as their rawValue internally, but the #Predicate macro doesn’t auto-bridge that conversion at the expression level.

The Fix: rawValue Stored Property Pattern

@Model
class Document {
    var statusRawValue: String   // #Predicate compatible
    
    var status: DocumentStatus {  // convenience computed
        get { DocumentStatus(rawValue: statusRawValue) ?? .inbox }
        set { statusRawValue = newValue.rawValue }
    }
    
    init(title: String, imageData: Data) {
        self.statusRawValue = DocumentStatus.inbox.rawValue
        // ...
    }
}

Now #Predicate gets a plain String comparison:

@Query(
    filter: #Predicate<Document> { $0.statusRawValue == "inbox" },
    sort: \Document.createdAt,
    order: .reverse
) private var documents: [Document]

App code still uses the type-safe document.status = .archived interface. Only storage and queries go through the rawValue.

Pitfall 2: modelContext Is nil on Empty @Query Results

What I Wrote

I needed a ModelContext in InboxView to save scanned documents. My first instinct:

struct InboxView: View {
    @Query(...) private var documents: [Document]
    
    func saveDocument() {
        if let context = documents.first?.modelContext {
            // save document...
        }
    }
}

When the Inbox is empty — like on first launch — documents is an empty array. documents.first is nil. No modelContext. A first-time user literally can’t save their first scan.

The Fix: @Environment

struct InboxView: View {
    @Query(...) private var documents: [Document]
    @Environment(\.modelContext) private var modelContext
    
    func saveDocument() {
        // always available
        let document = Document(title: "New", imageData: data)
        modelContext.insert(document)
    }
}

@Environment(\.modelContext) is always available as long as a modelContainer is injected into the view hierarchy. It doesn’t depend on query results.

Takeaways

  • #Predicate only compares primitive types. To filter by enum, keep a rawValue stored property alongside a computed enum accessor.
  • Never get modelContext from @Query results. Empty lists mean nil context. @Environment(\.modelContext) is the reliable path.

Neither of these pitfalls is clearly documented. You find out when the compiler tells you — or worse, when a user reports that saving doesn’t work on an empty list.