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.

4.5 (12) · 5 Reviews

Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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:

  1. Creates a binding for the currently selected sort item.
  2. 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:

  1. Builds a Menu to list the sort options.
  2. Presents a Picker and passes the selectedSortItem as the binding.
  3. Uses the sorts array as the source of data for the picker.
  4. Presents the name of the FriendSort as the menu item text.
  5. Shows a view with an icon and the word Sort as the label.
  6. 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:

Sort Preview

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:

  1. Instead of separating ToolbarItem wrappers, it embeds the two views for the toolbar in a ToolbarItemGroup and applies .navigationBarTrailing placement. The ToolbarItemGroup cuts down on a little bit of unnecessary code.
  2. It adds a SortSelectionView as the first toolbar item. Passes in selectedSort property as the binding for the PickerView.
  3. On change of the selected sort, it gets the SortDescriptors from the selected sort and applies them to the fetched friends list.
  4. Inserts the Add button toolbar element after the Sort view.

And that completes your sort implementation! Build and run to admire your handiwork.

Friends list showing the new sort button

Tapping the new sort button triggers a menu. The current sort is pre-selected:

New sort menu showing

Selecting a new sort will dismiss the menu and immediately sort the list:

Friends list with new sort applied

Opening the menu again shows the correct selected sort. Awesome!

Sort menu showing new sort selection

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:

  1. Creates a binding on the searchTerm property.
  2. Whenever searchQuery changes, it updates searchTerm.
  3. 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.
  4. 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!

Friends list being filtered by entering a search term

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:

  1. Switches to the new property wrapper @SectionedFetchRequest.
  2. 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 to Hashable.
  3. sortDescriptors and animation stay the same as before.
  4. Updates the friends property to include a generic parameter type for the section. In this case, it is String.

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:

  1. It iterates over the sectioned fetch results and performs work on each section.
  2. 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.
  3. For each row in the section, it creates a FriendView the same way you did before.
  4. In the .onDelete action, it passes the section along with the indexSet 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!

Friends list divided into sections by meeting place

You can also expand and contract sections in the list by tapping on the disclosure indicators:

Sections in the friends list in a collapsed state

However, something isn’t right. Next up, learn about an extra consideration you need when working with sectioned data.