← Blog

Core Data in SwiftUI

Core data uses persistent data storage to save an application’s information onto the device itself, that is, the data is available offline.

Step 1: Create data model

  1. Create a data model file that will store our entities and attributes

  1. Add entities and attributes. Given e.g below is a boolean value which is not optional and has “No” as the default value.

Step 2: Load model and actual data

  1. Create a data controller file of type swift file.

  2. Create a class DataController of type ObservableObject

  3. Initialise a container variable to NSPersistentContainer

NSPersistentContainer is responsible for loading a data model and give us access to entities and attributes.

The NSPersistentContainer take as parameter the data model file created in step 1 rather than the entity file.

import CoreData
import Foundation

class DataController: ObservableObject {
	let container = NSPersistentContainer(name: "MealModel")
}
  1. Load the actual data

To have access to the actual data store in the user’s device, we call loadPersistentStore on our container

The loadPersistentStore takes as handler: description and error. This is used to display any kind of error encountered when loading the data.

init() {
  container.loadPersistentStores { description, error in
     if let error = error {
	print("Failed to load data: \(error.localizedDescription)")
     }							   
}

Step 3: Preparation to use in SwiftUI

Apps work with only one core data store at a time. Rather than each view creating their own store, our app creates it once when the app starts, stores it in swiftUI environment and make it accessible everywhere in our app.

We first create an instance of DataController and introduce it into our SwiftUI environment.

In the main file

struct MealCoreData: App  {
// Creating instance
	@StateObject private var dataController = DataController()
    var body: some Scene {
        WindowGroup {
		// Introduction in environment
            ContentView().environment(\.managedObjectContext, dataController.container.viewContext)
				
        }
    }
}

Tip: if using previews, managed object context should be injected into the preview struct for that particular view.

Special Note: Managed Object Context deals with live data. I.e it only exist in Ram as long as the app is running, once the app closes it loses its data. Therefore to store it permanently, it needs to be saved into the persistent store.

Advantage: Using live data is less time consuming and save resources as it foregoes the need of reading from and writing to disk constantly. Instead, it only does it once the data is actually saved.

Step 4: CRUD

Reading from and writing to core data is done through FetchRequest.

FetchRequest takes at least one parameter that describe how we want the data to be sorted.

@FetchRequest(sortDescriptors: []) var meals: FetchedResults

That creates a fetch request with no sorting, and places it into a property called meals that has the type FetchedResults.

Now meals can be manipulated as a simple swift array.

Let’s create a simple UI for meal, so we can perform CRUD.

struct ContentView: View {
    let title = ["Burger", "Spicy Burger", "Pizza", "Spaghetti", "Chicken BBQ", "Lamb chop", "Chop suey", "Soya beans", "Baked beans", "Plain rice"]

   @FetchRequest(sortDescriptors: []) var meals: FetchedResults

    var body: some View {
        VStack {
	   List(meals) { meal in
		Text(meal.title ?? "Unknown")
	   }
	   Button {
	       let chosenMeal = title.randomElement()!		
	   } label: {
		Text("Add Meal")
	   }
	}// VStack
    }
}

As mentioned earlier, Managed Object Context(MOC) is used to save/delete data from store. So, to use MOC we need to declare it:

@Environment(\.managedObjectContext) var moc

Now let’s try using moc to save the data we declared and selected.

Button {
   let chosenMeal = set.randomElement()
   let meal = Meal(context: moc)
   meal.title = "\(chosenMeal ?? "Unknown")"
   try? moc.save()
} label: {
   Text("Add Meal")
}

Special Note: Each view using moc need to be added to the environment.

Filtering core data

We have a model entity called Beverage, which have as attributes title and categories both of type String.

Step 1: Create the MOC environment in the view

@Environment(\.managedObjectContext) var moc

Step 2: Read data from model using fetch request

@FetchRequest(sortDescriptors: [], predicate: nil) var Beverages: FetchedResults

Step 3: Create view to write data and save to MOC

var body: some View {
    VStack {
	List(Beverages, id:\.self) { beverage in
	  HStack {
		Text(beverage.title ?? "Unknown name")
		  .font(.title3)
		Spacer()
		Text(beverage.categories ?? "Unknown")
		  .font(.body)
		  .foregroundColor(.gray)
					
	  }//HStack
	} //List
	Button("Add Examples") {
	 let beverage1 = Beverage(context: moc)
	     beverage1.title = "Coffee"
	     beverage1.categories = "Hot"
				
	 let beverage2 = Beverage(context: moc)
	     beverage2.title = "Peach QQ"
	     beverage2.categories = "Cold"
				
	 let beverage3 = Beverage(context: moc)
	     beverage3.title = "Sprite"
	     beverage3.categories = "Fizzy"
				
	 let beverage4 = Beverage(context: moc)
	     beverage4.title = "Water"
	     beverage4.categories = "Room temp"
			
            // Save to database	
	 try? moc.save()

	}//Button
  }//Vstack
}//body

No sorting

Step 4: Learn how to filter data using NSPredicate property wrapper

  1. Filter data where categories = cold
@FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "categories == 'Cold'")) var Beverages: FetchedResults

Filtered data

  1. Filtering data where title = pimm’s cup
@FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "title == 'pimm's'")) var Beverages: FetchedResults

This results in

WHY THIS ERROR?

Because there is an in the beverage’s name

To be able to parse string data that contain ‘ we do it as follows:

@FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "title == %@", "Pimm's Cup")) var Beverages: FetchedResults

the ‘%@’ represents → whatever the string inserted here and the string inserted here is written as the second argument of NSPredicate

Price added to attribute so we can provide an example of using comparison operators for sorting

// ... more code here
	beverage1.price = 110			
        beverage2.price = 175
	beverage3.price = 30
	beverage4.price = 20
	beverage5.price = 100
	beverage6.price = 275
	beverage7.price = 275
				
// try? moc.save()				

No sorting

  1. Sorting beverages whose prices are less and equal to 100
@FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "price <= %@", "100")) var Beverages: FetchedResults

  1. Sorting beverages that can be in different categories
 @FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "categories IN %@", ["Alcohol based", "Cocktail"])) var Beverages: FetchedResults

  1. Sorting beverages with title starting with a particular character
  • Case sensitive
@FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "title BEGINSWITH %@", "P")) var Beverages: FetchedResults

  • Case insensitive
@FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "title BEGINSWITH[c] %@", "c")) var Beverages: FetchedResults

  1. Sorting by title containing case insensitive character c
@FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "title CONTAINS[c] %@", "c")) var Beverages: FetchedResults

Filtering a search bar using FetchRequest

struct ViewSix: View {
   @State private var searchText = ""
   @Environment(\.managedObjectContext) var moc
   @FetchRequest(sortDescriptors: []) var beverages: FetchedResults
	
   var body: some View {
     VStack{
	  List {
	    ForEach(beverages) { beverage in
		Text(beverage.title ?? "" )
	    }
	     .listRowSeparator(.hidden)
	 }
           .listStyle(.plain)
     }//Vstack
	.navigationTitle("Beverages")
	.searchable(text: $searchText)
	.onChange(of: searchText, perform: { searchText in
			
	    if searchText.isEmpty { 
               print("Empty")
	    } else {
	        beverages.nsPredicate = NSPredicate(format: "title     CONTAINS[c] %@", searchText)
	    }//else
	})//OnChange
  }//body
}//ViewSix