Episode #567

SwiftData - The Basics

Series: Leveraging SwiftData for Persistence

15 minutes
Published on November 14, 2023

This video is only available to subscribers. Get access to this video and 585 others.

SwiftData is a replacement for CoreData, built entirely for Swift. It leverages the underpinnings of Core Data, but is much simpler to work with. In this video we will cover the first 3 main types you'll need to understand: ModelConfiguration, ModelContainer, and ModelContext.

This episode uses Swift 5.8, Ios 17.0, Xcode 15.0.

Hello and welcome to this series on Swift Data. The goal of the series is to

make you productive in Swift Data so that you can write an app that persists data to

disk. Now Swift Data is brand new and it requires iOS 17, Mac OS 14 or watch OS 10

and so if you're working on a Mac app you're going to be you're going to need

to be on Mac OS Sonoma and I am NOT so we'll be doing everything inside of iOS

but it's basically the same concept across all these platforms it also

supports tvOS and vision OS and so the goal of this series is to cover all of

the basics so that we can understand what the various pieces are but also

understand how to work with this inside of a real app so we'll be building an

app later on but first let's take a look at the fundamental concepts of Swift

Data. So Swift Data under the hood is going to persist things to disk using a

SQLite database and in fact it uses the same underpinnings that powers core data

so there's a rich history and a lot of support including you know syncing with

cloud kit and and sharing a container between an app and a widget for instance

and so under the hood it'll be a SQLite database but you're not going to be

writing SQLite or SQL queries directly in fact there is also an in-memory

version of this persistent model but to decide how our model is set up we're

going to be working with a model configuration so starting at the bottom

here a model configuration is sort of the first thing that we need this is

going to take in schema which defines which models we are going to be storing

those models are classes and those will be turned into tables in the SQLite

sense but again we're not really supposed to think about it in terms of a

database you're supposed to think about it in terms of your models your

persistent models so so we have a schema that contains the models that we want

and those will automatically be converted into the representation in

SQLite so the tables and columns and things like that model configuration

that's also where you would specify where the database lives on disk we

don't have to specify this there is a default which will end up in your

application support directory you can also configure it to be in memory in

which case there will not will not be a SQLite database on disk and this is

really useful for our testing and Xcode previews and then we also have the

option to make a model read-only so saving isn't possible and this would be

useful if you're going to ship a database with your app you want to be

able to filter it and query it etc but you don't need intend for the users to

be saving records inside of that database so that's the model

configuration once we have a model configuration we can create a model

container now a model container contain can contain one or more configurations

and that means that we could have more than one database sort of aggregated

together in one model container you could store this set of models in this

database and this set of models in this database one could be read-only one

could be in memory so this is sort of our way to sort of group those things

together it also specifies how we're gonna migrate between one version and

another we're gonna talk a little more about this in detail later but a

migration is important because you're gonna have a database on disk in a

user's device and then later when you have a new version of the application

you may have added models changed the change the name of models you may have

deleted stuff or refactored and depending on how complex that changes

you may have to specify how do you translate from one model to the others

when a user has a database with data in it already and then finally this model

container is our entry point into getting a model context and if we're

using SwiftUI there's a handy modifier we can use to automatically provide this

context to the SwiftUI environment so that all of your SwiftUI views can

talk to the database okay and then the model context is our primary interface

for interacting with Swift data we're gonna use this to fetch insert update

and delete records and it also has undo and redo support so with that out of the

way let's take a look at Xcode I've got a super basic project here this is the

main entry point to the project the UI is not important right now and I've

created a model here we're gonna talk more about models later but notice that

this is all it takes to take this person class and to turn it into a model that

can be persisted using Swift data so I'm going to set up a task here so that we

have a place to run some code when our app launches and in that task we're

going to call a function and we're just going to call setup database and I'll

call that here setup database okay so the first thing we're going to do is

we're going to create our model configuration and if we look at the

options here this is the most detailed one so I'm just going to expand this one

and here we have our model configuration can take a name a schema memory only

allows save group container in fact let's move all of these to their own

line so our model configuration name this is going to be let's call this

Swift data basics this is optional you don't need to specify this at all we do

need to have a schema here and the schema is how we can determine what

entities we have in our persistent store so this is things like your models

things like relationships and so here we're going to say that we've got a

schema which is person dot self and then we can pass that schema here in memory

only is going to be false allow save will be true and those are the defaults

we also don't need to specify a group container unless we intend to share this

with with another application and then cloud kit database if we want to have

this sync between between devices okay so this is our model configuration it's

pretty basic and we can print out the configs URL to get the database URL I

want to actually get the path which is not percent encoded because I don't want

to get a I want something I can use in finder so that we can see what this

looks like okay so that's our configuration next is the container and

I'm gonna create a model container so I should specify what I'm doing here is

specifying everything out explicitly and the reason is is that it's important for

that we understand how do we configure these things the default way of or the

easiest way of doing this is saying that you want a model container and you can

pass in person dot self here and that's basically all you need for the most

basic example but if you want to configure things or do anything advanced

like migrations or you know anything else it's it's worth knowing how

everything is set up under the hood there's a lot of magic in this line here

so now that we have that let's create our model container and here we can pass

in the optional migration plan if we want to we'll talk more about migrations

later and then we have a again we have to specify the persistent models that we

want and I I believe the reason is that because we could be merging multiple

configurations we could have a person in one model we could have I don't know

jobs and another model or something in our case we just have one so we end up

repeating it here this can also fail because there's actually going to be

work done when creating this and for this demo I'm just going to force unwrap

so it crashes if there's something wrong here also this isn't an array it's in a

variadic argument so we can just pass in a single one if we had more we would

pass them in with commas okay so that is our container and at this point I want

to let's just run this and see what it does I'm going to set a breakpoint here

and we're going to run it okay so we have our path of where the database is

going to be if I step over this line of code that is what will create the

database and I'm going to copy this path here we're gonna hop over to finder and

paste in that path and we can see now that we have our Swift data basic store

and this is a SQLite database and I can open that my favorite application for

doing this is the base application which you can find on the App Store and if I

open this in base you can see here that some of this may look familiar if you're

familiar with core data you can see that this is definitely using that same the

same underpinnings the same exact structure a lot of things are prefixed

with Z which you don't really have to worry about because you're not really

intended to work with this directly but I find that is really useful to know

what's happening under the hood which will help you build a better mental

model of how to work with Swift data in the future especially when it comes to

things like migrations so we can see that we have some some stuff here that

sort of seems like we didn't create any of this but then we have a Z person and

this is our entity or our model that we created we can see it's got a primary

key we've got some other things that I don't really understand it's got an age

that was converted to an integer and a name that was converted to a varchar and

neither of those are nullable which is great because it matches the Swift type

and this is sort of one of the gripes of core data is the optionality and Swift

did not really translate perfectly to optionality or nullability in a core

data database so this is this is definitely an improvement and this is

you know what our database looks like so let's really quickly let's go back over

to to Xcode here and I'm going to create a person so let's create a person we'll

make this Ted lasso I don't know how old he is but maybe 48 and now I need to

insert this into the database and we do that with a context so I need to get a

context somehow and container actually has a main context it is async because

the main context should only be used on the main actor so if I were to change

this to an async function now I can await it this will present another

problem and sort of more specific to Swift concurrency but this is a really

important thing to note is that awaiting this and receiving the results could end

up putting this context on a different thread or a different actor and the main

context has to be accessed from the main actor so we can't we can't take this

main context it's not sendable which means it can't cross an actor boundary

so what we need to do instead or in addition to that is say that our

function runs on the main actor in which case we no longer need to await this

main context because we're already on the main thread of the main actor so

with that in mind now that we have a context we can say context dot insert

and we can insert our person and this one that one should work but then

there's a context dot save and this one throws and this is what is actually

going to turn our Swift data you know model into a SQL statement that will

update the SQLite store so let's go ahead and run this again we'll go all

the way to the end and then I need to add a weight here because we turn that

into an async function and then if I step over this we can see in here if I

go over to data we can see now we have a new row and that's our Ted Lasso row so

pretty basic but you know we've got to start from the very beginning so we

understand exactly how we can you know insert somebody into this database so

one other thing I want to point out is if we go over here to our person and I

say favorite color is a string I have to make this an optional because there's

already data in the database so let's do well we can just we can just leave it

like this okay so if I if I were to make it non-optional what is it going to do

with the data that exists already in the database it's not going to be able to

add a new column that can't contain a null value because our existing record

doesn't know what the favorite color is so when we're adding properties we'll

either need to delete the database which is something we might do often in

development or we make it optional and it can infer what we want to do based on

that we could also delete the age property and that also will work so

let's change our model to this I'm going to get rid of the age property and I

don't actually need to provide the favorite color I just wanted to add

something so we could see what happens here and now if I run it this will end

up creating another record so let's go over to base again and reload notice

that the age was dropped and that's fine as long as you're okay with that data

being gone because it's now gone from our model and it's easy for the database

to say oh I know how to delete a column but what it doesn't know how to do is

add a new column that has a non nullable value that we haven't ever specified so

those are some things just to keep in mind as you're changing your model if

you add new columns and you don't want to deal with migrations you're gonna

have to make them optional so that existing databases still work okay so

that's a tour of Swift data basics in the next episode we're going to dive

deeper into the model and what exactly is this model macro.