iOS Core Data Explained: Storing data using Swift

iOS Core Data Explained: Storing data using Swift

iOSSwiftSwitUI
Fix bugs faster! Log Collection Made Easy
START NOW!

Core Data enables us to manage the model layer of an Apple application. This layer is a crucial part of our app’s engine room, allowing the pretty bits at the front end to interact with the data and business logic at the back.

We love Core Data because it provides a powerful database technology framework, and it’s built on top of the SQLite management system, which requires zero configuration or external storage space. It also runs on an Object Oriented interface, which is great for re-usable code.

Like all frameworks, however, Core Data framework takes some time to master. So in this post, we’re going to give you the building blocks of your knowledge, looking at specific themes such as how to manage data, how to set up entities and how to add, delete, and modify data.

This piece on Core Data is part of our comprehensive iOS Data Persistence series, which delves into the various data storage options accessible to iOS developers on mobile devices. Our series also explores Swift Data, Realm, and other relevant technologies in detail.

First, what are the benefits of Core Data?

There’s a whole bunch of stuff we could prise open here, but this is a tutorial, not a pitch. So we’ll just give you a few top-line advantages that you’ll find when you bring Core Data into your life.

  • Abstraction: As Core Data abstracts the underlying Data Model, developers are able to work with high-level object-oriented representation of the data
  • Storage and Retrieval: Core Data allows for efficient data storage and retrieval, particularly when dealing with large datasets.
  • Relationship Management: Core Data provides a simple way of handling relationships between entities in a Data Model.
  • Querying and Filtering: Core Data‘s querying and filtering mechanism uses the NSPredicate class, allowing developers to easily filter and fetch data from the persistent store.

Plus all the benefits we mentioned at the top. So it’s pretty useful, really.

Now, let’s get started on setting up Core Data

To set up Core Data, we’ll first need our model, which we’ll create following the steps below:

  1. In Xcode, click on File, then choose New, and File.
  2. There, choose Data Model under the Core Data section.
  3. Name the Data Model, and click create.

This video demonstrates the process:

iOS Core Data Project

Simple, right? Now we have our Core Data model.

Creating our Core Data Stack

In this article we’ll be using SwiftUI, the first language of modern Apple development. So let’s have a look at setting up our Core Data stack.

First we’ll create a new file (File > New > File > Swift File) called DataController. Once we’ve created the DataController, the code should be as follows:

import CoreData

class DataController: ObservableObject {
    let container = NSPersistentContainer(name: "MyDatabase")
    
    init() {
        container.loadPersistentStores { description, error in
            if let error = error {
                print("Core Data failed to load: \(error.localizedDescription)")
            }
        }
    }
}

Next, we’ll head to the root of our app and add the following code to the launch:

struct PersistenceApp: App {
    @StateObject private var dataController = DataController()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, 
															dataController.container.viewContext)
        }
    }
}

This is pretty much ‘boilerplate’ code, and it won’t change much when we use different Core Data models (in other models, we’d simply need to change the MyDatabase to match the name of the respective model).

So we now have our Core Data stack to use throughout our app.

Creating our database, entities and models

Now we’ve created the stack, we can create our database. This is what it will look like:

It’s a simplified version of a social media Post on a social app and we’ll be persisting three objects:

  1. A Post that will have a title, author, and a list of likes .
  2. A User that will have a username, and follow other Users .
  3. A Like, that has a certain like date and is associated with a User.

Creating our entities

Now we know what our entities will look like, we can get to the business end and start creating them on our database.

We’ll start by selecting the MyDatabase file we created earlier and, once selected, we’ll have the Add Entity button available.

iOS Core Data Add Entity

We can now go ahead and create the entities we mentioned earlier.

Let’s start with the User. The user will have one Attribute (the username), and a one-to-many relationship with the User entity.

This is how we add a user and set the respective attributes and relationships:

Core Data model

Now, we can do the same for the other two entities:

Like will have one attribute date, and a one-to-one relationship with the User:

Code Data Relationship

Post, will have one attribute title, and two relationships:

  • Author: a one-to-one relationship with User .
  • Likes: one-to-many relationship with Like .
Core Data Relationship

So, all our entities are now set up and ready to use. And here’s the fun part: now we get to create the models to use with them, so there’s no need to be worried about creating managed objects anymore, Xcode will do it for us.

Creating our models

We could create our models manually. However, that’s not necessary, because Xcode has the functionality to create them for us with all the necessary Core Data stack, so there’s no need to be worried about creating managed objects anymore.

We simply need to select our database, then select Editor on the top navigation bar and Create NSManagedObject Subclass. Then just follow the steps provided by Xcode:

iOS Core Data Model Creation

That’s it! We have created a Core Data entity for each of our models. We know, from the questions we get at Bugfender, that a lot of readers get het up about Core Data and worry about its complexity. But actually, as you’ll see, most of the foundations are pretty straightforward.

Let’s take a look at some basic Core Data operations

Now we’ve set up our stack and models, we can start looking at how we can use them to store, read and delete data. This is another part of the process you’ll really enjoy.

Adding data to Core Data

We’ll see a simple screen with a button on top. This button will create a random User and save it in our Core Data database.

To do this, our View needs to access the Core Data managed object context (which is provided through an environment var), and add an element to our context, saving it afterwards.

We’ll use the following class to provide us with randomly generated usernames:

class RandomGenerator {
    static func username() -> String {
        let usernames = ["Quantum", "Giraffe", "Mystic", "Penguin", "Nebula", "Phoenix", "Zenith", "Whisper", "Radiant", "Tiger", "Lunar", "Cascade", "Celestial", "Breeze", "Ephemeral", "Dragon", "Cosmic", "Echo", "Enigma", "Sparrow"]

        let index1 = Int.random(in: 0...usernames.count - 1)
        let index2 = Int.random(in: 0...usernames.count - 1)

        return  "\(usernames[index1])\(usernames[index2])"
    }
}

Now that we can generate the usernames, our View will look like this:

import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) 
    var context
		// Here we access the Environment variable that provides us the Core Data Context
    
    var body: some View {
        VStack {
            Button(action: {
                let user = User(context: context)
								// We create a user in the given context
                user.username = RandomGenerator.username()
                try? context.save()
								// It can now be saved with our new user
            }, label:{
                Text("Add User")
            })
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

Each button tap will add a random user, but we’ll need to add a way to read the data to check whether it’s working.

Reading data from Core Data

To read the data, we’ll add a FetchRequest(to fetch data for us), and a List that shows all available users. This is what we add to our View:

import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) 
    var context
    
    @FetchRequest(sortDescriptors: [])
    var users: FetchedResults<User>
		// Our Fetch Request, that simply fetches all our Users

    var body: some View {
        VStack {
            Button(action: {
                let user = User(context: context)
                user.username = RandomGenerator.username()
                try? context.save()
            }, label:{
                Text("Add User")
            })
            
						List {
              ForEach(users, id: \.self) { user in
                  Text(user.username ?? "")
              }
            }
						// The list that will show all available users
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

We can now add users and read them, so the only basic operation we haven’t covered is deleting.

Deleting data from Core Data

To delete data from our Database, we just need to use a .delete:

ourContext.delete(ourObject)

To illustrate the process, let’s add a way to show a delete button on our list, and then delete the appropriate object:

List {
  ForEach(users, id: \.self) { user in
      Text(user.username ?? "")
  }
  .onDelete(perform: { offsets in
      context.delete(users[offsets.first ?? 0])
   })
}

And that’s the coding done! We now know how to set up basic operations, another major brick in the wall.

Testing our operations and app

We should now be able to add users, see the user list, and delete users from the list by dragging to the left, as shown below:

Now for some more advanced Core Data operations

Let’s have a look at some operations on a slightly more complex scale (if we’re going a bit too fast and you have questions, then you can always contact us with your query. [email protected]. Just so you know :)).

Generating data

First we’ll need to generate data for use in our demonstration examples, and for this we’ll need to completely change our RandomGenerator so it can generate more complex example structures.

Our new RandomGenerator will be:

class RandomGenerator {
    static func user(in context: NSManagedObjectContext, thatFollows follows: [User] = []) -> User {
        let usernames = ["Quantum",
                         "Giraffe",
                         "Mystic",
                         "Penguin",
                         "Nebula",
                         "Phoenix",
                         "Zenith",
                         "Whisper",
                         "Radiant",
                         "Tiger",
                         "Lunar",
                         "Cascade",
                         "Celestial",
                         "Breeze",
                         "Ephemeral",
                         "Dragon",
                         "Cosmic",
                         "Echo",
                         "Enigma",
                         "Sparrow"]
        
        let index1 = Int.random(in: 0...usernames.count - 1)
        let index2 = Int.random(in: 0...usernames.count - 1)
        
        let user = User(context: context)
        user.username = "\(usernames[index1])\(usernames[index2])"

        follows.forEach({
            user.addToFollows($0)
        })
        
        return user
    }
    
    static func posts(in context: NSManagedObjectContext) -> [Post] {
        var oneHundredUsers: [User] = []
        
        for _ in 1...100 {
            let otherUsers = Int.random(in: 0...100)
            
            var follows: [User] = []
            
            for _ in 0...otherUsers {
                follows.append(RandomGenerator.user(in: context))
            }
            
            oneHundredUsers.append(RandomGenerator.user(
                in: context,
                thatFollows: follows
            ))
        }
        
        let blogpostTitleArray = [
            "Resilience",
            "Serendipity",
            "Thrive",
            "Wanderlust",
            "Illuminate",
            "Empower",
            "Unleash",
            "Harmonize",
            "Catalyst",
            "Flourish",
            "Zenith",
            "Enchant",
            "Pinnacle",
            "Catalyst",
            "Odyssey",
            "Quench",
            "Jubilant",
            "Synergy",
            "Revitalize",
            "Traverse"
        ]
        
        var fiveHundredPosts: [Post] = []
        
        for _ in 1...500 {
            let index1 = Int.random(in: 0...blogpostTitleArray.count - 1)
            let index2 = Int.random(in: 0...blogpostTitleArray.count - 1)
            
            let post = Post(context: context)
            
            post.author = oneHundredUsers[Int.random(in: 0...oneHundredUsers.count - 1)]
            RandomGenerator.randomLikes(in: context,
                                        from: oneHundredUsers).forEach({
                post.addToLikes($0)
            })
            
            post.title = "\(blogpostTitleArray[index1])\(blogpostTitleArray[index2])"
            
            fiveHundredPosts.append(post)
        }
        
        return fiveHundredPosts
    }
    
    static func randomLikes(in context: NSManagedObjectContext, from users: [User]) -> [Like] {
        let amountOfLikes = Int.random(in: 0...users.count - 1)
        
        var likes: [Like] = []
        
        for likeIndex in 0...amountOfLikes {
            let like = Like(context: context)
            like.user = users[likeIndex]
            likes.append(like)
        }
        
        return likes
    }
}

Bulk adding data

Our new RandomGenerator can generate 500 posts at once and we can add all those objects to our database in bulk, like this:

let posts = RandomGenerator.posts()

posts.forEach({ element in
    context.insert(element)
})

try? context.save()

The process is similar to adding a single item. The key difference is that when adding a single item, we save the context after, but when adding in bulk we add them all to the context first, and only after they’ve all been added do we save the context.

More complex queries

Now let’s create some more complex queries. To do this our base View (which will show us the Post name, author name, and amount of likes), will be as follows:

struct ContentView: View {
    @Environment(\.managedObjectContext)
    var context
    
    @FetchRequest(sortDescriptors: [])
    var posts: FetchedResults<Post>
    
    
    var body: some View {
        VStack {
            Button {
                
                let posts = RandomGenerator.posts(in: context)
                posts.forEach({ element in
                    context.insert(element)
                })
                try? context.save()
                
            } label: {
                Text("Add 500 posts")
            }
            
            List {
                ForEach(posts, id: \.self) { post in
                    VStack {
                        Text(post.title ?? "").font(.system(size: 24))
                        HStack {
                            Text(post.author?.username ?? "")
                            Spacer()
                            Text((post.likes?.count.description ?? "") + " likes")
                        }
                    }                }
            }
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

Right, we’re really on a roll now. So let’s look at some examples of how we can change our queries to better suit our needs:

Sorting data

First, let’s look at sorting the data by author and alphabetically. To do this we’ll use a sort on our list, which means changing our FetchRequest as follows:

@FetchRequest(sortDescriptors: [
        NSSortDescriptor(key: "author.username", ascending: true)
    ])
    var posts: FetchedResults<Post>

We should now see our Posts grouped by user and in alphabetical order.

Filtering data

If we wanted to filter the data (e.g. so we only see posts from ‘EchoLunar’), we can use a Predicate to specify what data we would like to fetch, like this:

@FetchRequest(predicate: NSPredicate(
													format: "author.username == %@", "EchoLunar")
							)
var posts: FetchedResults<Post>

We can use SortDescriptors to sort by a specific keypath, Predicate clauses for filtering the objects we want to fetch, or even combine the two for greater flexibility.

Bulk deleting data

Ok, now let’s say we wanted to delete all posts with a specific title. For this, we’d change our .onDelete modifier to the following:

.onDelete { indexSet in
                    posts.filter {
                        $0.title == posts[indexSet.first ?? 0].title
                    }.forEach {
                        context.delete($0)
                    }
                    try? context.save()
                }

Now, we can delete all posts with the same title with one click.

Our final View

Now we’re at the final stage in the funnel. We hope it’s been a fun tour!

To take a look at our final View, we can:

  • Add 500 posts with one click of a button
  • Filter to only shows posts by ‘EchoLunar’
  • Order alphabetically by author
  • Bulk delete every post with the same title
struct ContentView: View {
    @Environment(\.managedObjectContext)
    var context
    
    @FetchRequest(sortDescriptors: [
        NSSortDescriptor(key: "author.username", ascending: true)
    ], predicate: NSPredicate(
        format: "author.username == %@", "EchoLunar")
    )
    var posts: FetchedResults<Post>
    
    
    var body: some View {
        VStack {
            Button {
                
                let posts = RandomGenerator.posts(in: context)
                posts.forEach({ element in
                    context.insert(element)
                })
                try? context.save()
                
            } label: {
                Text("Add 500 posts")
            }
            
            List {
                ForEach(posts, id: \.self) { post in
                    VStack {
                        Text(post.title ?? "").font(.system(size: 24))
                        HStack {
                            Text(post.author?.username ?? "")
                            Spacer()
                            Text((post.likes?.count.description ?? "") + " likes")
                        }
                    }
                }.onDelete { indexSet in
                    posts.filter {
                        $0.title == posts[indexSet.first ?? 0].title
                    }.forEach {
                        context.delete($0)
                    }
                    try? context.save()
                }
            }
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

To sum up

Core Data is a database framework built on top of SQLite, which provides an Object Oriented interface to manage data. And it’s awesome, for all the reasons we mentioned at the top.

To use Core Data in an app, we should:

  • Create/add a Data Model (File > New > File > Data Model).
  • Add entities to the Data Model that match the objects we’d like to use.
  • Create NSManagedObject subclasses for our entities.
  • Create a DataController class that provides am environmental context anywhere in the app.

Now we can:

  • Use our database context anywhere:
@Environment(\.managedObjectContext)

var contex
  • Read the records for any View with a FetchRequest:
@FetchRequest var myCollection: FetchedResults<MyType>

(using myCollection now fetches all records of a given type)

  • Add elements to (or delete them from) our context:
context.insert(myElement)

context.delete(myElement)
  • Save our context to make sure everything is persisted:
try? context.save()
  • Sort our FetchRequests with SortDescriptors, or filter them with Predicates, for more complex operations.

While it may be more complex than SwiftData, Core Data is a great database solution to use. It’s been around for longer and is tried and tested on thousands of apps, so you know it’s going to stand up to anything. Have fun using it!

Expect the Unexpected! Debug Faster with Bugfender
START FOR FREE

Trusted By

/assets/images/svg/customers/highprofile/gls.svg/assets/images/svg/customers/projects/ultrahuman.svg/assets/images/svg/customers/cool/ubiquiti.svg/assets/images/svg/customers/highprofile/rakuten.svg/assets/images/svg/customers/highprofile/tesco.svg/assets/images/svg/customers/cool/airmail.svg/assets/images/svg/customers/highprofile/credito_agricola.svg/assets/images/svg/customers/projects/menshealth.svg

Already Trusted by Thousands

Bugfender is the best remote logger for mobile and web apps.

Get Started for Free, No Credit Card Required