Logo for tanaschita.com

Keeping SwiftData behind a boundary

Learn how to keep SwiftData out of SwiftUI views.

13 Jul 2026 · 5 min read

SwiftData integrates naturally with SwiftUI. It lets us build persistence into an app quickly, but it also makes it easy to mix persistence details directly into views.

In this article, we'll look at how we can create a boundary between the UI and the persistence layer when working with SwiftData.

Sponsorship logo
Architecture & Design Patterns for iOS
This book is a practical guide to essential architectural principles and design patterns for iOS development. It covers strategies for building maintainable apps with Swift and SwiftUI, including dependency injection, navigation, common patterns, and modularization.
LEARN MORE

A view can read from a ModelContext, use @Query, create model objects, insert them, delete them, and save the context. For a small prototype this can feel very convenient. In a real app, however, this kind of direct access can become one of the first places where responsibilities start to blur.

The direct version of a SwiftData-backed view often starts like this:

struct ItemListView: View {
@Environment(\.modelContext) private var modelContext
@Query private var items: [PersistedItem]
var body: some View {
List(items) { item in
Text(item.timestamp, format: .dateTime)
}
.toolbar {
Button("Add") {
modelContext.insert(PersistedItem(timestamp: Date()))
}
}
}
}

This is compact, and SwiftData makes the persistence work almost invisible. The tradeoff is that the view now knows quite a lot about how data is stored. If we later add validation, error handling, sorting rules, sync behavior, test data, or a different persistence strategy, the view might take on too many responsibilities.

To keep responsibilities clean, we can put a small persistence boundary between the UI-facing code and the storage. We can make the persisted type explicit:

@Model
final class PersistedItem {
var id: UUID
var timestamp: Date
init(id: UUID = UUID(), timestamp: Date) {
self.id = id
self.timestamp = timestamp
}
}

The UI-facing model can then be a plain value type:

struct Item: Identifiable, Equatable, Sendable {
let id: UUID
let timestamp: Date
}

PersistedItem tells us that this type belongs to the storage layer. Item is the value the feature works with. This makes the boundary visible in the code and reduces the chance that a persistence model slowly spreads through views, navigation state, previews, and tests.

The repository protocol can describe the feature's persistence needs without exposing SwiftData:

@MainActor
protocol ItemRepository {
func fetchItems() throws -> [Item]
func addItem(timestamp: Date) throws
}

The view model depends on this boundary instead of depending on SwiftData directly:

@MainActor
@Observable
final class ItemListViewModel {
private let repository: any ItemRepository
private(set) var items: [Item] = []
init(repository: any ItemRepository) {
self.repository = repository
}
func loadItems() throws {
items = try repository.fetchItems()
}
func addItem() throws {
try repository.addItem(timestamp: Date())
try loadItems()
}
}

Now the view can stay focused on presentation and user interaction. It displays viewModel.items and calls methods such as viewModel.loadItems() or viewModel.addItem(), but it no longer knows about ModelContext, fetch descriptors, or SwiftData insert calls.

The SwiftData-specific implementation moves into the repository:

@MainActor
final class ItemRepositoryImpl: ItemRepository {
private let modelContext: ModelContext
init(modelContext: ModelContext) {
self.modelContext = modelContext
}
func fetchItems() throws -> [Item] {
let descriptor = FetchDescriptor<PersistedItem>(
sortBy: [SortDescriptor(\.timestamp, order: .forward)]
)
return try modelContext.fetch(descriptor).map { persistedItem in
Item(id: persistedItem.id, timestamp: persistedItem.timestamp)
}
}
func addItem(timestamp: Date) throws {
modelContext.insert(PersistedItem(timestamp: timestamp))
try modelContext.save()
}
}

This stronger boundary comes with a tradeoff: we now have two types and mapping code between them. For very small prototypes, that can feel like extra work. In an app that is meant to grow, however, the advantages are often worth it:

  • PersistedItem makes it clear which type belongs to SwiftData.
  • Item can be a simple value type that is easier to pass around, compare, preview, and test.
  • Item can conform to Sendable, which fits better with Swift concurrency than passing persistence-managed reference objects through the app.
  • Migration concerns stay closer to the persistence layer.
  • The repository becomes the place where storage details, mapping, fetch descriptors, inserts, and saves are handled.

With a boundary in place, the view model can be tested with an in-memory or fake repository, previews can provide controlled data, and the persistence implementation can evolve in a controlled way.

Sponsorship logo
Preparing for a technical iOS job interview
Preparing for a technical iOS Job Interview with over 300 questions & answers. Covering Swift & Objective-C, SwiftUI & UIKit, Combine, HTTP Networking, iOS File System, Core Data, Concurrency with async/await, Security, Automated Testing, Dependency Management, AI & Machine Learning and more.
LEARN MORE
Sponsorship logo
Architecture & Design Patterns for iOS
This book is a practical guide to essential architectural principles and design patterns for iOS development. It covers strategies for building maintainable apps with Swift and SwiftUI, including dependency injection, navigation, common patterns, and modularization.
LEARN MORE
Sponsorship logo
Become a sponsor of tanaschita.com
By publishing an article on different iOS topics every week, tanaschita.com is constantly growing in the developer community and may provide a great audience for you as a sponsor.
CLICK TO LEARN MORE

Newsletter

Image of a reading marmot
Subscribe

Like to support my work?

Say hi

Related tags

Articles with related topics

swiftdata

swift

ios

How to define one-to-many relationships in SwiftData

Learn to use SwiftData's @Relationship macro.

13 Jul 2026 · 4 min read

Latest articles and tips

© 2026 tanaschita.com

Privacy policy

Impressum