How to implement SwiftData relationships in SwiftUI
Decorate the parent model's child array with @Relationship(deleteRule: .cascade, inverse: \Child.parent) and SwiftData automatically manages the link, cascade deletes, and CloudKit-compatible inverse references. Both model types must be registered in the same ModelContainer schema.
import SwiftData
@Model final class Author {
var name: String
@Relationship(deleteRule: .cascade, inverse: \Book.author)
var books: [Book] = []
init(name: String) { self.name = name }
}
@Model final class Book {
var title: String
var author: Author?
init(title: String) { self.title = title }
}
// In App entry point:
// .modelContainer(for: [Author.self, Book.self])
Full implementation
The example below models a simple library app with Author and Book entities in a one-to-many relationship. The AuthorListView shows all authors and lets you drill into a detail view that displays and adds books for that author. Deleting an author cascades to its books automatically.
import SwiftUI
import SwiftData
// MARK: - Models
@Model final class Author {
var name: String
var createdAt: Date
@Relationship(deleteRule: .cascade, inverse: \Book.author)
var books: [Book] = []
init(name: String, createdAt: Date = .now) {
self.name = name
self.createdAt = createdAt
}
}
@Model final class Book {
var title: String
var year: Int
var author: Author? // inverse side — SwiftData owns this link
init(title: String, year: Int) {
self.title = title
self.year = year
}
}
// MARK: - Author List
struct AuthorListView: View {
@Environment(\.modelContext) private var context
@Query(sort: \Author.name) private var authors: [Author]
@State private var newAuthorName = ""
var body: some View {
NavigationStack {
List {
ForEach(authors) { author in
NavigationLink(author.name) {
AuthorDetailView(author: author)
}
}
.onDelete(perform: deleteAuthors)
}
.navigationTitle("Authors")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Add Author") { addAuthor() }
}
}
}
}
private func addAuthor() {
let author = Author(name: "New Author \(authors.count + 1)")
context.insert(author)
}
private func deleteAuthors(at offsets: IndexSet) {
for index in offsets {
// Cascade delete removes all books automatically
context.delete(authors[index])
}
}
}
// MARK: - Author Detail (books for one author)
struct AuthorDetailView: View {
@Environment(\.modelContext) private var context
let author: Author
var body: some View {
List {
ForEach(author.books.sorted(by: { $0.year < $1.year })) { book in
VStack(alignment: .leading) {
Text(book.title).font(.headline)
Text(String(book.year)).font(.caption).foregroundStyle(.secondary)
}
}
.onDelete(perform: deleteBooks)
}
.navigationTitle(author.name)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Add Book") { addBook() }
}
}
}
private func addBook() {
let book = Book(title: "Untitled \(author.books.count + 1)", year: 2026)
author.books.append(book) // SwiftData inserts via the relationship
}
private func deleteBooks(at offsets: IndexSet) {
let sorted = author.books.sorted(by: { $0.year < $1.year })
for index in offsets {
context.delete(sorted[index])
}
}
}
// MARK: - App Entry Point
@main struct LibraryApp: App {
var body: some Scene {
WindowGroup {
AuthorListView()
}
.modelContainer(for: [Author.self, Book.self])
}
}
// MARK: - Preview
#Preview {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try! ModelContainer(for: Author.self, Book.self,
configurations: config)
let ctx = container.mainContext
let tolkien = Author(name: "J.R.R. Tolkien")
ctx.insert(tolkien)
tolkien.books.append(Book(title: "The Hobbit", year: 1937))
tolkien.books.append(Book(title: "The Fellowship of the Ring", year: 1954))
return AuthorListView()
.modelContainer(container)
}
How it works
-
@Relationship(deleteRule: .cascade, inverse: \Book.author)— Placed onAuthor.books, this macro tells SwiftData to delete every childBookwhen its parentAuthoris deleted, and to treatBook.authoras the inverse pointer so both sides stay in sync automatically. -
Implicit insertion via
author.books.append(book)— You don't need to callcontext.insert(book)explicitly. When a model object is appended to a relationship array that is already tracked by aModelContext, SwiftData inserts it into the context automatically, keeping the store consistent. -
@Query(sort: \Author.name)— SwiftData's@Queryproperty wrapper onAuthorListViewobserves the persistent store and re-renders whenever authors are inserted or deleted — no manualfetchRequestboilerplate required. -
In-memory preview container — The
ModelConfiguration(isStoredInMemoryOnly: true)in the#Previewblock creates an isolated, disposable store so previews never pollute the device's real database. -
Schema registration order — Both
Author.selfandBook.selfare passed toModelContainer(for:). SwiftData only needs the root type to discover related models transitively, but listing both is explicit, avoids migration surprises, and is required when the child type is the only entry point for a query.
Variants
Many-to-many: Tags on Books
SwiftData handles many-to-many relationships with two arrays — one on each side — and a @Relationship on one of them specifying the inverse. SwiftData generates the hidden join table for you.
@Model final class Tag {
var label: String
// Inverse side — no @Relationship needed here
var books: [Book] = []
init(label: String) { self.label = label }
}
@Model final class Book {
var title: String
var year: Int
var author: Author?
// Many-to-many: a book can have many tags
@Relationship(inverse: \Tag.books)
var tags: [Tag] = []
init(title: String, year: Int) {
self.title = title
self.year = year
}
}
// Usage:
let fiction = Tag(label: "Fiction")
context.insert(fiction)
book.tags.append(fiction) // also adds book to fiction.books
Nullify instead of cascade
Use deleteRule: .nullify (the default) when child records should survive the parent's deletion — for example, a Comment that should remain after its Post is removed. SwiftData will set the inverse optional to nil rather than deleting the row. Use deleteRule: .deny to throw an error if you attempt to delete a parent that still has children, surfacing referential integrity violations at the app layer.
Common pitfalls
- iOS 17 minimum — no back-deploy: SwiftData and
@Relationshipare unavailable below iOS 17. Add an availability guard (@available(iOS 17, *)) or keep a parallel Core Data stack if you must support iOS 16. - Missing inverse causes silent data loss: If you omit the
inverse:key path, SwiftData may create a unidirectional link that breaks cascade deletes or CloudKit sync. Always declare both sides of the relationship explicitly. - Sorting relationship arrays in-view, not in-query:
author.booksis an unsorted array at the model layer. Sort it at the call site (e.g.,.sorted(by:)) rather than assuming insertion order. For large collections, push filtering to a@Querywith aPredicateon the child type to avoid loading every related object into memory. - Accessing relationships off the main actor:
ModelContextis notSendable. Reading relationship arrays on a background thread will trigger a runtime warning or crash. Use a separateModelActorfor background work and pass value types back to the main context.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement SwiftData relationships in SwiftUI for iOS 17+. Use SwiftData/@Relationship with deleteRule: .cascade and explicit inverse key paths. Model a one-to-many and a many-to-many relationship. Make it accessible (VoiceOver labels on list rows and buttons). Add a #Preview with realistic in-memory sample data.
Drop this prompt into Soarias during the Build phase after your data model is sketched in the mockup stage — Claude Code will scaffold both the @Model classes and the SwiftUI views in one shot, leaving you to wire up business logic.
Related
FAQ
Does this work on iOS 16?
No. SwiftData — including @Model and @Relationship — requires iOS 17 or later. If you need to support iOS 16, you must use Core Data with NSManagedObject subclasses and @NSManaged relationship properties. You can bridge both frameworks in the same project using ModelContainer's Core Data store underneath, but you cannot use SwiftData APIs on iOS 16 devices.
Can I filter a child collection with a Predicate instead of loading all records?
Yes — and you should for large datasets. Instead of accessing author.books directly, use a separate @Query on Book with a predicate: #Predicate<Book> { $0.author?.name == authorName }. This pushes filtering to SQLite and avoids faulting every related object into memory, which is critical for collections with hundreds or thousands of rows.
What's the Core Data / UIKit equivalent?
In Core Data you model the same relationship in the .xcdatamodeld editor, mark both sides as a relationship with an inverse, and set "Cascade" as the delete rule on the parent entity. In code you access children via an @NSManaged var books: NSSet property (or a typed NSOrderedSet) and insert objects by calling managedObjectContext.insert(_:) explicitly. SwiftData eliminates the visual editor and NSManagedObject subclass boilerplate entirely.
Last reviewed: 2026-05-11 by the Soarias team.