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
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.