This lab focused on SwiftData and Core Data topics: widgets and app intents, large datasets, previews and sample data, Results Observer, aggregate queries, nonoptional model creation, CloudKit, app groups, schema evolution, counting, performance, sync behavior, Codable types, multiple model contexts, grouped queries, and model actors.
As usual, the goal is simple: make the questions easier to scan, easier to revisit, and easier to connect with real app development problems.
I tried to preserve the original wording and combine related answers where appropriate. However, some inaccuracies or mismatches are still possible.
Enjoy! And subscribe so you don’t miss the next Lab.
Use an app group when the app and widget or extension need to share the same SwiftData store.
The panel recommended making the main app the process that owns migration. Widgets and extensions can read and write the shared database after the app has prepared it, but they should not be responsible for schema migration.
For widgets and extensions, do not include the schema migration plan. If the widget starts before the user launches the updated app, use the error path to show UI asking the user to open the app first so the app can run migration.
The Sample Trips app was mentioned as an example that includes a widget and uses a group container to share data.
First identify what “large” means for the app.
If the app has many rows, index the fields used for filtering and sorting. Also use fetch limits and predicates so the app does not pull more objects into memory than it needs.
If each row contains large binary data, use external storage for large data blobs. That allows the database to keep the bytes outside the main store file.
For large imports, use smaller batches and short-lived model contexts. Insert a batch, save it, discard that context, then continue with the next batch. This keeps memory pressure lower and avoids one context accumulating too much work.
Use Instruments to validate assumptions. The hitches instrument can show UI problems, and the persistence instrument can show what SwiftData is actually fetching under the hood.
For previews, use preview traits and seed expressive data into an in-memory or preview-specific container.
The Sample Trips app uses preview traits to seed preview data. The panel recommended making preview data diverse enough to exercise the UI: long names, many rows, and different shapes of data.
You can also run a query inside a preview and use its result to drive the previewed view.
For migration testing, sample data matters in a different way. Keep a corpus of previous stores from earlier app versions. Test migration from each shipped schema, not only the most recent one, because users can update after skipping versions for a long time.
The panel also mentioned that AI coding tools can be useful for generating varied sample data.
Use Results Observer when you want an observable fetch outside a SwiftUI view.
@Query is tied to SwiftUI’s view lifecycle. Results Observer gives similar observable-fetch behavior in places such as view models or other non-view code.
Performance is broadly similar because the underlying fetch behavior is similar. The main difference is the API shape and whether it participates directly in SwiftUI lifecycle.
History Observer is different: it helps catch up with store changes over time. It is useful when you need to replicate changes, filter by author, or know which changes happened since a point in time.
Both Results Observer and History Observer reduce the need to manually listen for notifications and decide when to refetch.
For min and max, a fetch limit of one plus a sort descriptor can solve many cases.
For more general aggregate expressions such as sums and averages, the panel acknowledged this is a gap compared with Core Data’s older NSExpression workflows. They asked developers to file feedback with concrete use cases, not only “I used this before.”
If an app needs an aggregate feature that SwiftData does not currently expose, one workaround is coexistence: use SwiftData and Core Data stacks pointing at the same store, and use Core Data for the missing aggregate operation.
If a SwiftData model has nonoptional properties, set them in the initializer.
If a view needs a single existing model object, it is often cleaner to fetch that object outside the child view. The parent can decide whether to show the real view, create the object, or show an unavailable state.
For one-object fetches, use ModelContext directly with a fetch limit instead of forcing the view body to handle an empty @Query array. That keeps creation and lookup logic outside the SwiftUI rendering path.
The panel’s general advice was to give the view a real model to work with instead of making the body contain too much “does this exist yet?” logic.
A versioned schema can be added later.
For app groups, moving the store changes the store location. If the app previously used the default model configuration, SwiftData can move the data for you. If the app previously specified a custom store URL, then the app needs to move the data itself.
To introduce versioned schemas, start with the schema the app currently has and make that the first versioned schema. From there, build future versions and migration plans progressively.
The panel said sample code for this migration path would be useful feedback.
Be careful about which process owns CloudKit sync.
If multiple apps, widgets, or extensions share a SwiftData store in an app group, they all need appropriate entitlements if they are expected to sync the CloudKit-backed store.
But widgets and extensions often should not be the processes doing CloudKit sync. They have less time and different runtime constraints. The panel suggested a cleaner pattern: let the main app own the CloudKit-syncing store, and use a separate local app-group store when extensions or widgets need local shared data.
This avoids requiring a widget to perform expensive sync work and keeps sync behavior in the process best suited for it.
Make sure the apps agree on schema.
The panel warned against competing migrations. You do not want one app adding a column while another app removes it, or different targets shipping incompatible model definitions.
During development, learn from testing, align schemas across apps, and eventually push the schema to production. Then test production CloudKit sync as well, not only development sync.
The short version from the panel: sync is hard, so keep the schema story disciplined.
Use fetchCount on ModelContext.
If the app only needs a count, it should not fetch all objects and count them in memory. Ask the model context for the count directly.
For performance-sensitive views, it is also fine to fetch manually outside @Query when that gives you more control. But if you cache manually, you must own cache invalidation.
The panel recommended combining the SwiftUI instrument and the persistence instrument. Sometimes the expensive-looking problem is I/O, but the real cause is a SwiftUI view invalidating too often and triggering repeated fetches.
First make sure the data is actually saved.
Autosave can take a few seconds, and if you stop the app from Xcode before autosave fires, the data may never be saved and therefore cannot sync. During testing, background the app instead of force-stopping it.
Check entitlements carefully, especially differences between debug and release configurations. If pushes are not arriving, that can affect sync timing.
Different devices also have different policies. Apple Watch may be more conservative about networking, especially off the charger or on cellular. Phones may throttle work under thermal pressure.
If sync behaves unexpectedly, collect sysdiagnose from all relevant devices and file feedback so Apple can inspect what happened.
Raw-representable enums are improving, including predicate support this year.
For associated values or types SwiftData cannot map cleanly into a schema, Codable can work as long as the values are Codable. New this year, developers can explicitly mark attributes to be treated as Codable.
The tradeoff is queryability. If a value is stored as encoded data, SwiftData cannot reason about the inner structure for predicates and sorting.
If you need to query or sort by parts of the value, model those parts explicitly with SwiftData models and relationships, then expose an enum-like computed API to the rest of the app.
The panel mentioned types like MapKit data or Foundation Measurement as examples where Codable storage can be useful when you do not own the underlying type or cannot model it directly.
Model contexts are working sets and transaction boundaries.
Use separate contexts when work is meaningfully independent, especially background work or batch imports. Keep background contexts short-lived so they do not accumulate too much memory.
Do not assume “more contexts” always means “more performance.” The panel emphasized that I/O is often the bottleneck, and too much concurrency can add coordination overhead, memory pressure, and diminishing returns.
SQLite allows multiple readers and one writer with WAL journaling. That means concurrent reads can happen while a write is active, but write coordination still matters.
Benchmarking is hard because caches exist at many layers: SQLite page cache, file-system cache, and storage-controller behavior. Use the I/O instrument and persistence instrument to understand what is actually happening.
Be specific in the fetch.
Use predicates, sort descriptors, fetch limits, and identifiers rather than pulling large object graphs into memory. Matthew’s analogy from the panel: do not ask the library reference desk for every book and then sort through them yourself. Ask for the books you actually need.
If you only need identifiers, fetch identifiers. If you only need a count, fetch the count. If you only need a subset, make the predicate describe that subset.
This reduces memory pressure, avoids unnecessary I/O, and makes SwiftUI-driven views less likely to amplify performance problems.
SwiftData now has better support for sectioned / grouped observable fetches.
The panel referenced the new results observer support for sectioning. This helps when developers previously had to fetch data and manually group it themselves for UI presentation.
Sectioning is useful when the UI naturally groups objects, such as by date, category, trip, or another derived grouping key.
The performance guidance stays the same: make sure the grouping and filtering are represented in the fetch as much as possible, index where appropriate, and use Instruments if the grouping operation appears expensive.
Use model actors to isolate SwiftData work across concurrency domains.
The panel discussed the importance of not casually passing live model objects across concurrency boundaries. SwiftData models are tied to their model context, and contexts are not something you should freely share across unrelated tasks or actors.
A model actor gives you a structured place to perform database work. Instead of passing full model instances everywhere, pass identifiers or values, then fetch or operate on the model inside the actor or context that owns the work.
This keeps concurrency safer and makes it clearer which context owns each operation.
A huge thank-you to everyone who joined and asked practical SwiftData questions throughout the session. Your questions made the discussion useful for developers working with SwiftData, Core Data, widgets, app intents, large datasets, previews, migrations, Results Observer, CloudKit, app groups, performance, sync, Codable values, multiple contexts, grouped queries, and model actors.
Question acknowledgments: the developers who asked about widgets and App Intents, large datasets, sample data, Results Observer, aggregate queries, nonoptional model creation, CloudKit, app groups, counting, sync behavior, enums, Codable types, multiple model contexts, grouped queries, and actor isolation.
Finally, a heartfelt thank-you to Kurt, Rishi, David, Thomas, Ben, and the teams behind the scenes for sharing practical SwiftData and persistence guidance.
No posts

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.