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. By Mark Struzinski.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Dynamic Core Data with SwiftUI Tutorial for iOS
20 mins
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
Menuto list the sort options. - Presents a
Pickerand passes theselectedSortItemas the binding. - Uses the
sortsarray as the source of data for the picker. - Presents the name of the
FriendSortas 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
ToolbarItemwrappers, it embeds the two views for the toolbar in aToolbarItemGroupand applies.navigationBarTrailingplacement. TheToolbarItemGroupcuts down on a little bit of unnecessary code. - It adds a
SortSelectionViewas the first toolbar item. Passes inselectedSortproperty as the binding for thePickerView. - On change of the selected sort, it gets the
SortDescriptors from the selected sort and applies them to the fetchedfriendslist. - 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
searchTermproperty. - Whenever
searchQuerychanges, 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
meetingPlaceas the section identifier. The section identifier can be any type you would like, as long as it conforms toHashable. -
sortDescriptorsandanimationstay the same as before. -
Updates the
friendsproperty 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
Sectioncontainer 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
FriendViewthe same way you did before. - In the
.onDeleteaction, it passes thesectionalong with theindexSetso 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.







