Dynamic Core Data with SwiftUI Tutorial for iOS
Learn how to take advantage of all the new Core Data features introduced in iOS 15 to make your SwiftUI apps even more powerful.
Version
- Swift 5.5, iOS 15, Xcode 13

iOS 15 offers some powerful new features for SwiftUI when used with Core Data. You can now dynamically apply sorts and filters to lists and see the results update immediately. iOS 15 also brings some ease-of-use updates to SwiftUI, such as more straightforward configuration.
In this tutorial, you’ll learn about all the exciting new features released with iOS 15 for SwiftUI apps that use Core Data.
You’ll learn how to:
- Update existing fetch requests with new sort criteria.
- Perform sectioned fetch requests.
- Apply dynamic filters with predicates.
There’s a lot to cover; time to get started!
Getting Started
Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.
In this tutorial, you’ll build a Core Data app that helps you track information about your best friends — your besties! Don’t worry if you’re the lonesome type, the app comes with a set of pre-loaded imaginary friends for you to play with.
Open the starter project and take a peek around. The app — called Besties — lets you track your friends’ names, where you met them and the date you met. It uses Core Data for persistence.
Build and run to see the app in action:
You can tap the + button on the top right to add a new Bestie, if the built-in ones don’t do it for you:
After you save, the new Bestie shows up in your list:
You can remove a friend, if they were mean, by swiping to delete, or you can edit one by tapping them, but the main point of this tutorial is to work with the list, so you’ll have a quick tour of the code to orient yourself, then get started.
Getting to know your Besties
Open PersistenceManager.swift. This class handles Core Data setup and save operations. The setup is standard — you initialize an NSPersistentContainer
and use that to load the persistent stores. You won’t need to make any changes to this class for the tutorial.
Open AppMain.swift. You’ll see the persistenceManager
property is initialized immediately and then passed into the SwiftUI environment for use in child views. This is also the point where, if you don’t have any, the sample friends are added to the database.
Finally, open ContentView.swift. You’ll see a standard implementation of an @FetchRequest
property wrapper to fetch a list of Besties sorted by name:
@FetchRequest(
sortDescriptors: [
NSSortDescriptor(
keyPath: \Friend.meetingDate,
ascending: true)
],
animation: .default)
private var friends: FetchedResults<Friend>
You’ll adapt this view throughout the tutorial to perform dynamic sorting, add search via dynamic predicates and update the list to add sections for organization.
Setting Up Core Data
iOS 15 adds a small convenience with lazy entity resolution. Prior to iOS 15, you would need to set up your Core Data stack before adding it to the environment. If a view attempted to access the view context before setup, you would most likely see a crash. With iOS 15, lazy entity resolution solves that problem. You can remove some boilerplate code because of this.
Open AppMain.swift. Locate and delete the following property near the top of the class:
let persistenceManager = PersistenceManager.shared
Core Data will now lazily initialize entities when needed, so this property doesn’t have to be there to “warm up” the Core Data stack. Update the environment variable in AppMain
to the following:
.environment(
\.managedObjectContext,
PersistenceManager.shared.persistentContainer.viewContext)
You are now accessing the view context directly from PersistenceManager
and adding it to the environment. This allows ContentView
, and all its child views, to use the view context from the .managedObjectContext
environment variable.
Build and run, and the app will function just as it did before.
Next, you’ll move on to creating dynamic sorting behavior.
Exploring Dynamic Sorting
Dynamic sorting is another new feature in iOS 15. With dynamic sorts, you can apply new sort descriptors directly to your list of fetched items backed by the @FetchRequest
property wrapper. Updating the sort descriptors will cause the view to refresh immediately. You can apply animation by default as well.
Setting Up the Sort Model
First, create a model object representing a single sort. In the Xcode Project navigator on the left sidebar, open the Model group. Create a new Swift file and name it FriendSort.swift.
Add the following after import Foundation
:
// 1
struct FriendSort: Hashable, Identifiable {
// 2
let id: Int
// 3
let name: String
// 4
let descriptors: [SortDescriptor<Friend>]
}
This structure defines the following:
- Conform to
Hashable
andIdentifiable
. This is necessary to useFriendSort
in a SwiftUI view for selecting sorts from a menu. - Add
id
to conform toIdentifiable
. -
name
is the friendly name of the sort shown in the sort menu. -
descriptors
are theSortDescriptor
s to apply.
SortDescriptor
object vs. the older NSSortDescriptor
. This is new in iOS 15.
Add a list of pre-cooked sort options for the user to choose from, by adding the following static properties to FriendSort
:
// 1
static let sorts: [FriendSort] = [
// 2
FriendSort(
id: 0,
name: "Meeting Place | Ascending",
// 3
descriptors: [
SortDescriptor(\Friend.meetingPlace, order: .forward),
SortDescriptor(\Friend.name, order: .forward)
]),
FriendSort(
id: 1,
name: "Meeting Place | Descending",
descriptors: [
SortDescriptor(\Friend.meetingPlace, order: .reverse),
SortDescriptor(\Friend.name, order: .forward)
]),
FriendSort(
id: 2,
name: "Meeting Date | Ascending",
descriptors: [
SortDescriptor(\Friend.meetingDate, order: .forward),
SortDescriptor(\Friend.name, order: .forward)
]),
FriendSort(
id: 3,
name: "Meeting Date | Descending",
descriptors: [
SortDescriptor(\Friend.meetingDate, order: .reverse),
SortDescriptor(\Friend.name, order: .forward)
])
]
// 4
static var `default`: FriendSort { sorts[0] }
Here’s what’s happening with the code above:
- It adds a static property to allow easy access to sorts from a SwiftUI View.
- It returns an array of
FriendSort
items. - Each
SortDescriptor
specifies the keypath to sort on and the order in which to sort. Note the newSortDescriptor
API introduces the.forward
and.reverse
enumeration values instead of the old Boolean-based sort direction specifier. Each option has a primary sort descriptor and a second one for cases where the first value is the same, such as when you meet lots of people in the same place. - This static property is used as the initial sort option.
Take a moment to update the @FetchRequest
property wrapper in ContentView.swift with the newer SortDescriptor
API. Find this code at the top in @FetchRequest
:
sortDescriptors: [
NSSortDescriptor(
keyPath: \Friend.meetingDate,
ascending: true)
],
Replace it with this:
sortDescriptors: FriendSort.default.descriptors,
Now you are using the sort descriptors from the default sort option.
Setting Up the Sort View
Next, you’ll set up a view to present the sort menu. In File navigator, create a new SwiftUI view in the Views group and name it SortSelectionView.swift.
At the top of the file, just under the struct
declaration, add the following:
// 1
@Binding var selectedSortItem: FriendSort
// 2
let sorts: [FriendSort]
The above code does the following:
- Creates a binding for the currently selected sort item.
- Creates the array to provide the list of sorts to the view.
Next, update your preview by providing a selected sort. Right under the declaration of SortSelectionView_Previews
, add the following property:
@State static var sort = FriendSort.default
This property creates the initial data required by SortSelectionView
. Next, update the initializer for SortSelectionView
inside the preview. Replace SortSelectionView()
with the following:
SortSelectionView(
selectedSortItem: $sort,
sorts: FriendSort.sorts)
The code above passes the sort
property in as a binding and a list of sorts from FriendSort
, satisfying the compiler and rendering your preview. You may need to start the preview canvas by clicking the Resume button or using the keyboard shortcut Command-Option-P.
Finally, replace Text("Hello, World!")
in the body of SortSelectionView
with the following:
// 1
Menu {
// 2
Picker("Sort By", selection: $selectedSortItem) {
// 3
ForEach(sorts, id: \.self) { sort in
// 4
Text("\(sort.name)")
}
}
// 5
} label: {
Label(
"Sort",
systemImage: "line.horizontal.3.decrease.circle")
}
// 6
.pickerStyle(.inline)
The code above does the following:
- Builds a
Menu
to list the sort options. - Presents a
Picker
and passes theselectedSortItem
as the binding. - Uses the
sorts
array as the source of data for the picker. - Presents the name of the
FriendSort
as the menu item text. - Shows a view with an icon and the word Sort as the label.
- Sets the picker style to
.inline
, so it displays a list immediately without any other interaction required.
Great! This completes the sort menu view. Click the Live Preview button in the SwiftUI preview canvas to see your results. Tap Sort to see your menu presented:
Connecting the Sort View
Next, it’s time to connect SortSelectionView
to ContentView
. Open ContentView.swift. Right under friends
, add the following:
@State private var selectedSort = FriendSort.default
The code above adds a state property that represents the selected sort option and uses the default value you defined earlier.
Finally, replace the body of the .toolbar
modifier with the following:
// 1
ToolbarItemGroup(placement: .navigationBarTrailing) {
// 2
SortSelectionView(
selectedSortItem: $selectedSort,
sorts: FriendSort.sorts)
// 3
.onChange(of: selectedSort) { _ in
friends.sortDescriptors = selectedSort.descriptors
}
// 4
Button {
addViewShown = true
} label: {
Image(systemName: "plus.circle")
}
}
Here’s what’s happening with this code:
- Instead of separating
ToolbarItem
wrappers, it embeds the two views for the toolbar in aToolbarItemGroup
and applies.navigationBarTrailing
placement. TheToolbarItemGroup
cuts down on a little bit of unnecessary code. - It adds a
SortSelectionView
as the first toolbar item. Passes inselectedSort
property as the binding for thePickerView
. - On change of the selected sort, it gets the
SortDescriptor
s from the selected sort and applies them to the fetchedfriends
list. - Inserts the Add button toolbar element after the Sort view.
And that completes your sort implementation! Build and run to admire your handiwork.
Tapping the new sort button triggers a menu. The current sort is pre-selected:
Selecting a new sort will dismiss the menu and immediately sort the list:
Opening the menu again shows the correct selected sort. Awesome!
When you have this many friends, though, you can’t always find the one you want by sorting. Next, you’ll find out how to add a search.
Implementing Search and Filter
Now, you’ll implement search and live filtering. First, you need to add an @State
property to hold the value of the current search. In ContentView.swift, add the following directly under selectedSort
:
@State private var searchTerm = ""
Next, under searchTerm
, create a binding property that will handle updating the fetch request:
var searchQuery: Binding<String> {
Binding {
// 1
searchTerm
} set: { newValue in
// 2
searchTerm = newValue
// 3
guard !newValue.isEmpty else {
friends.nsPredicate = nil
return
}
// 4
friends.nsPredicate = NSPredicate(
format: "name contains[cd] %@",
newValue)
}
}
The code above does the following:
- Creates a binding on the
searchTerm
property. - Whenever
searchQuery
changes, it updatessearchTerm
. - If the string is empty, it removes any existing predicate on the fetch. This removes any existing filters and displays the complete list of Besties.
- If the search term isn’t empty, it creates a predicate with the search term as criteria and applies it to the fetch request.
Finally, add a searchable
modifier to the List
view right before the .toolBar
modifier:
.searchable(text: searchQuery)
This modifier binds the search field’s value to the searchQuery
property you just created. This connects your search field to a dynamic predicate on your fetch request.
Build and run and give your new search a try. Once the list of Besties displays, pull down to expose the search field. Start typing a search, and you’ll see the list filter based on the contents of the search field. Excellent!
Next, learn about another way to make your list more useful by dividing it into sections.
Updating to Sectioned Fetch Requests
With iOS 15, Apple has added the ability to render sections in your SwiftUI view right from the fetch request. This is done with a new type of fetch request property wrapper named @SectionedFetchRequest
.
@SectionedFetchRequest
requires generic parameters for the type of data that represents your sections and the type of Core Data entity that will compose your list. The sectioned request will give you section separators with titles in your list and will even allow collapsing and expanding sections by default.
Open ContentView.swift and replace the entire friends
property and @FetchRequest
property wrapper with the following:
// 1
@SectionedFetchRequest(
// 2
sectionIdentifier: \.meetingPlace,
// 3
sortDescriptors: FriendSort.default.descriptors,
animation: .default)
// 4
private var friends: SectionedFetchResults<String, Friend>
The code above does the following:
-
Switches to the new property wrapper
@SectionedFetchRequest
. -
Provides a keypath for your section identifier. Here, you’ll use
meetingPlace
as the section identifier. The section identifier can be any type you would like, as long as it conforms toHashable
. -
sortDescriptors
andanimation
stay the same as before. -
Updates the
friends
property to include a generic parameter type for the section. In this case, it isString
.
Because you are switching to a sectioned fetch, you need to update the method signature of deleteItem(for:section:viewContext:)
in ListViewModel
to account for the addition of sections in the list. Open ListViewModel.swift and update the section
argument to receive an item from the section:
section: SectionedFetchResults<String, Friend>.Element,
Finally, update ContentView.swift to render the sections along with the FriendView
for each row. Replace everything inside of List
with the following:
// 1
ForEach(friends) { section in
// 2
Section(header: Text(section.id)) {
// 3
ForEach(section) { friend in
NavigationLink {
AddFriendView(friendId: friend.objectID)
} label: {
FriendView(friend: friend)
}
}
.onDelete { indexSet in
withAnimation {
// 4
viewModel.deleteItem(
for: indexSet,
section: section,
viewContext: viewContext)
}
}
}
}
Here’s what’s going on with the code above:
- It iterates over the sectioned fetch results and performs work on each section.
- It creates a
Section
container view for each result section. It uses a text view with the value of the section ID for display. In this case, it will be the name of the meeting place. - For each row in the section, it creates a
FriendView
the same way you did before. - In the
.onDelete
action, it passes thesection
along with theindexSet
so that you can locate and delete the correct row.
That covers basic support for adding sections to your fetch request. Build and run. You’ll see that your list now has built-in sections by meeting place!
You can also expand and contract sections in the list by tapping on the disclosure indicators:
However, something isn’t right. Next up, learn about an extra consideration you need when working with sectioned data.
Updating Sorts
Trying to sort right now won’t work at all. Try changing the sort options — eventually, the app will crash and display this message:
Thread 1: "UITableView internal inconsistency: encountered out of bounds global row index while preparing batch updates"
This error is caused by a mismatch in section and row data when you sort on one property but group on another. The fetch results must be sorted in section order for the list to work. To fix this, you need to add an additional property to your sort option struct
. Open FriendSort.swift and add a new property to FriendSort
:
let section: KeyPath<Friend, String>
Next, update sorts
to add the new section
parameter to all of the initializers:
static let sorts: [FriendSort] = [
FriendSort(
id: 0,
name: "Meeting Place | Ascending",
descriptors: [
SortDescriptor(\Friend.meetingPlace, order: .forward),
SortDescriptor(\Friend.name, order: .forward)
],
section: \Friend.meetingPlace),
FriendSort(
id: 1,
name: "Meeting Place | Descending",
descriptors: [
SortDescriptor(\Friend.meetingPlace, order: .reverse),
SortDescriptor(\Friend.name, order: .forward)
],
section: \Friend.meetingPlace),
FriendSort(
id: 2,
name: "Meeting Date | Ascending",
descriptors: [
SortDescriptor(\Friend.meetingDate, order: .forward),
SortDescriptor(\Friend.name, order: .forward)
],
section: \Friend.meetingDay),
FriendSort(
id: 3,
name: "Meeting Date | Descending",
descriptors: [
SortDescriptor(\Friend.meetingDate, order: .reverse),
SortDescriptor(\Friend.name, order: .forward)
],
section: \Friend.meetingDayDescending)
]
Each sort option now has a section key path. Note that the two meeting date options have different key paths — without this, you get a crash when switching between the two date options.
Go back to ContentView.swift. Here, you can replace the initial section with the new property from the default sort option. In the @SectionedFetchRequest
property, replace the section identifier with the following code:
sectionIdentifier: FriendSort.default.section,
Still in ContentView.swift, update the application of sorts to the list by replacing the body of the .onChange
closure with this::
let request = friends
request.sectionIdentifier = selectedSort.section
request.sortDescriptors = selectedSort.descriptors
This code might seem a little strange — why is let request = friends
there? Why not just update friends
directly? The reason is that you are now making two changes to the fetch request, but they both need to be evaluated at the same time. Each time you reference friends
, it’ll commit any changes to the fetch request. So if you change friends.sectionIdentifier
, then change friends.sortDescriptors
, the fetch request with the updated section identifier will be evaluated, before you have had chance to update the sort descriptors. As you’ve seen earlier, this can cause crashes, as the results have to be in section order. Pulling out the request into a local reference and updating that means that the changes won’t be evaluated until friends
is accessed again when the view body is recomputed.
Build and run to test your new changes. You’ll see sorting by meeting place will group by section and perform a sort on meeting place name.
Performing a sort by meeting date will change the grouping to meeting date, then perform a sort on that property.
Where to Go From Here?
Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.
If you want to dig deeper into working with Core Data and SwiftUI together, check out this Core Data with SwiftUI tutorial and this Core Data with CloudKit tutorial.
We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!
Comments