```html SwiftUI: How to Use SwiftData Relationships (iOS 17+, 2026)

How to implement SwiftData relationships in SwiftUI

iOS 17+ Xcode 16+ Advanced APIs: SwiftData / @Relationship Updated: May 11, 2026
TL;DR

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

  1. @Relationship(deleteRule: .cascade, inverse: \Book.author) — Placed on Author.books, this macro tells SwiftData to delete every child Book when its parent Author is deleted, and to treat Book.author as the inverse pointer so both sides stay in sync automatically.
  2. Implicit insertion via author.books.append(book) — You don't need to call context.insert(book) explicitly. When a model object is appended to a relationship array that is already tracked by a ModelContext, SwiftData inserts it into the context automatically, keeping the store consistent.
  3. @Query(sort: \Author.name) — SwiftData's @Query property wrapper on AuthorListView observes the persistent store and re-renders whenever authors are inserted or deleted — no manual fetchRequest boilerplate required.
  4. In-memory preview container — The ModelConfiguration(isStoredInMemoryOnly: true) in the #Preview block creates an isolated, disposable store so previews never pollute the device's real database.
  5. Schema registration order — Both Author.self and Book.self are passed to ModelContainer(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

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.

```