Episode #567
SwiftData - The Basics
Series: Leveraging SwiftData for Persistence

Episode #567
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.