Episode #568

The @Model Macro

Series: Leveraging SwiftData for Persistence

21 minutes
Published on November 16, 2023

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

In this episode we will take a look at the new @Model macro, which we can use to decorate our model classes that we want to persist with SwiftData.

This episode uses Xcode 15.0, Swift 5.9.

Okay, now that we know how to set up a model container,

let's take a look at the Swift data model.

Now, the model are the resources that you're going to be

saving or the records you're going to be

saving to persist your apps data.

So these are typical model objects

that you want to persist to a database.

In this demo, we're going to take a look at

a demo application called Songs to Play.

This is going to be a list of artists and

songs by that artist that I want to play in my cover band.

So let's take a look first at the artist model.

I'm going to create a name for the artists,

and I'm going to make it a var

because I want to be able to mutate it.

That's what we want to be able to do with our records.

We might bring a record from storage,

mutate the name and then save it.

Let's see, what else does an artist have

besides the list of songs?

Maybe we have something like is currently on tour,

and maybe that has a default value of false.

Here I'm going to decorate this with the model macro.

Then I need to also provide

an initializer because this is not a struct,

and we don't have a default value for this,

and so we have to be able to create one.

Here is the basics of our artist model.

This is a macro which is new in Swift.

What a macro will do is at compile time,

it can look at the thing it is attached to

and do interesting things like emit additional source code.

The nice thing about this is it all happens at

compile time and you can see the code that it generates.

What's happening here is if we go here and right-click on it,

and say Expand Macro,

this will show us all of

the generated code that we have inside of our macro.

You can see that on each one of our properties,

it added this macro underscore PersistedProperty.

Now we could go look and see what that looks like,

but I think it's probably pretty obvious that just

marking these as a PersistedProperty.

Then down here we have this new property that was created,

the private property called BackingData.

It is of some type called BackingData.

This is something akin to a dictionary,

so it can marshal values to and from the SQLite database.

This one is marked as transient,

which means that we can use this one ourselves.

The key difference being it's not prefixed with an underscore.

Transient is something we would use if we want to say that we just

want a property but we don't want that

to be persisted to the backing store.

There's another computed property and

computed properties do not get persisted.

They only stored properties

that are not marked as transient get persisted.

This one is a computed property for Persisted BackingData.

Then it has a static var schema metadata.

This is the information it needs to

generate the schema for the SQLite database.

You can see that it automatically

picked up each one of the properties.

It knows the key path, the default value.

It already knows the default value of

the artist is currently on tour is false and metadata is nil.

Then there's an init for creating this based on some backing data.

This is doing it basically by saying that

the backing data is going to be able to provide the name,

and the is currently on tour.

This is what the model macro is doing for us.

We can add additional things to this.

Let me right-click on this and say hide macro expansion.

Let's say now I want to have artists have an array of songs.

If I add this and we'll say that we started off as an empty array,

and we go over to song.

Now, if we look at this right now and

right-click on it and say expand macro,

this becomes a persisted property and songs is over here.

But it doesn't know about

this relationship because this one is not a model yet.

So we're going to have to go over here and say that that is a model.

Let's say a song has a title, which is a string.

It might have a year released,

and I'll just make that an integer.

This is optional because we may not want to specify it.

It might also have a duration,

which is a time interval, again, optional.

Then let's do that.

This also happens to have an artist.

So all songs belong to an artist.

We're going to have to put artist in here.

I'll move that to the top.

So this is our song model.

Now, if we take a look at the generated code here,

if I say expand macro,

the interesting thing here is,

well, it's basically the same as what we saw before.

So what we need to do now is on the artists model,

when we're looking at the model for songs,

we want the songs to be able to,

if you delete the artist,

for it to go ahead and delete all the songs.

What will happen by default is if you delete an artist,

it'll go through all the songs that belong to

that artist and nullify that relationship.

And we actually don't support that

because we can't nullify an artist here.

So what we want to do is we want to specify

the relationship macro.

And there are some options and a delete rule

and some other stuff here that you should take a look at.

But if we take a look at options,

the basic one is that the relationship is unique.

We don't really care about that,

but what we do care about is the delete rule.

And we want that to be cascade.

We also may want to specify that the inverse

of this relationship is the song.artist.

And that's what sort of tells you,

cause there's no other way for it to sort of figure this out

in some frameworks like Rails, for instance,

it would figure it out based on the name of this property,

but Swift data doesn't do anything like that.

And so in order for you to sort of tell it

that you want to cascade this

and that the inverse relationship is song.artist.

Now at a database level, we'll have to take a look

and see if it actually generates it this way.

But this would be the only way for you to find

the foreign key that you want to go then set up

so that when you delete a record from one row,

that it automatically deletes the other.

I'm not sure if Swift data actually uses this

under the hood or if they just do a delete for the artist.

And then immediately after that,

delete from songs where the artist ID equals some ID.

We can take a look at that.

Okay, so if we expand the macro now,

and we take a look down here,

songs has that relationship metadata attached to it.

So this is how it sort of can know

when it's generating the database.

So now that we have our model ready to go,

let's hide the macro expansion.

I'm gonna go over to our songs to play app,

which is a completely basic app.

And we're going to use the super basic

model container modifier that I mentioned last time

is nice for demos and nice for many apps,

but you may need to drop down

into passing in configurations and stuff.

So what we need to do is specify the types

that we're going to have in our model.

And we can put in artists.self here and song.self.

All right, cause those are our two models.

But in fact, if there's a relationship defined,

you can omit the sort of destination type

and Swift data is smart enough to say,

well, okay, there's a model container for the artist,

but the artist also refers to song.

And so I'm going to have a model container

for the song as well.

So if we do that, and then we go into our content view,

our content view now could use those model objects.

We're gonna talk more about this later,

but I will just say that we can pass in an environment

whose key path or whose environment key rather

is the model context.

And this is how the sort of views that are

inside of this hierarchy here will inherit

this model container and we'll be able

to get a model context.

So let's go ahead and run this so we can see what happens.

I actually also want to, let's see,

we'll just make a task here because remember last time

we had an easy way to get the configuration

so we could get the path.

I don't have that here, but we still can just print out,

say something like get the URL to our application support

directory by using file manager.default.urls

for application support directory in the user domain mask.

And let's grab the first one and then we'll just print out

that URL and I'll just force unwrap it.

So this will give us a way to see where this data

is being stored and I'm going to run it.

And this is our directory and now I didn't do

the percent encoded form so I'm going to just copy

the library portion of that.

Now we're gonna hop over to finder.

I'm gonna command shift G, paste that path in,

jump into application support and this is our database.

So now let's take a look at base and I'm going

to drag this in.

Now base is showing us that we have a Z artist.

We have is currently on tour.

Notice that we have a Boolean in our model,

but that translates to an integer in SQLite.

So zero being false and non-zero being true.

And then a name for Varchar.

Then we have a song and notice that we have year released,

which is an integer.

We have Z artist, which is an integer and this is the key

into this table here.

Now what I don't know and I'm guessing that this one,

we've got an index for the artist.

I think that the F stands for foreign key here,

which means that there is no foreign key constraint.

That's not really surprising because Swift data

is backed by core data's underpinnings and core data

wanted to handle the constraints and things like that

at the model layer, the application layer in your Swift code.

And not necessarily the database level.

And that's fine because we aren't going to be touching

this database directly.

This would be a problem if we had some Swift data models

on top of a database and then something else

that was also interacting with this database,

we'd want to make sure that if you deleted a song,

sorry, if you deleted an artist,

it would delete all of that artist songs.

But that's something that Swift data is going to manage

for us.

But this is another reason why I think it's really important

that we are familiar with loading up this database

to see what the actions, what are the results of our actions

that we take here.

For instance, this is how I know that if I just pass

an artist.self, it works and we get artist and song.

And this also means that if you want to use this form,

it could be an interesting idea for you to create a model

that is something like,

we can just try this here.

Maybe we call this model root.

So we've got a model class here.

And maybe this just has the list of all the artists

or all the root models in your application.

And that can be start off with an empty array.

Okay, so then we need our initializer here,

which can take an empty array.

We don't actually need to create that, right?

Because the model root is just there to say,

hey, I have artists.

Let's see what this generates.

I'm curious if this would be an interesting pattern to use,

just sort of list out your top level models in one place.

So we're gonna run it.

We're gonna jump back over to base and I will reload.

Now there is a model root table,

which we're not gonna use,

but because there's no,

okay, interesting.

So in the model root table,

because we had artists,

the artists table ended up with this extra thing.

So this is, yeah, maybe this isn't something

we want to do, but it is kind of interesting

as a way to do this.

Another way to think about this is to say that

maybe model root itself isn't a model,

but just contains the list of models that we,

the list of models that we want to leverage.

So maybe this ends up being like an enum,

so we can't create an instance of it.

And then this ends up becoming root models or something.

And then this one can have the list of models.

So artists.self.

This is kind of a similar idea,

but a way to specify that in one place.

And then down here, I could say model root.root models,

or something like that.

And this would essentially do the same thing

that we were doing before.

Now, interestingly enough,

as I've been playing around with this,

now, let's see, this needs to be,

this one is an artist.self.

This needs to be something like a persistent model.type.

And then that can equal artist.self.

And then here we will say for model root.root models.

Okay, and we just need the any keyword there.

Okay, so if we run this now, what ends up happening,

let's take a look at the database.

It did drop that column here,

but artists now has an artist, this artist thing,

which is not really what we want.

And so as we're playing around here,

we might either generate some stuff

that we don't really want in our database,

or we might actually run into an error

that we made a change that's sort of incompatible

with the current schema.

So when that happens, it can be advantageous to say,

that when your application boots up,

that you want a way to quickly wipe the database.

And the way I've done this in the past is to create a scheme.

So I'm going to option click on songs to play.

So we have this normal one that runs our application.

I'm gonna duplicate the scheme,

and I'm going to call this one like clean slate

or something.

So now I've got two schemes.

One of them is clean slate.

If I option click on clean slate and go over to the run tab,

we're going to, you can do this

with environment variables or arguments.

I'm gonna do it with an argument.

And this is going to be something like clean DB.

And let's see, I can't remember if I need the,

yeah, I believe I need the dash there.

Well, I don't, yeah, we can define it however we want to.

Okay, so now when we are setting up our task here,

I'm gonna do this before the model container.

And here I'm gonna say, if we're in debug mode,

and this is an important one

because you don't want to ship code that has this ability.

So I can take a look at process info,

dot process info dot contains, or sorry, arguments

dot contains, and we can pass in, let's see,

arguments is an array, contains a collection.

Let's see, what is our, arguments is an array of strings.

Yeah, so does it contain where dollar zero equals dash clean DB

and if it does, now we can actually delete

the application support directory.

So we can say try bang file manager

dot delete or default dot remove item at

and we have the URL, so we'll say remove item at URL.

I'm going to force unwrap it here just so that we have it.

This is in fact is always in debug,

so I'm just gonna move all that stuff in there.

Then we're gonna print it.

I'm also gonna do the path percent encoded false

so we have an easier string to copy in our terminal.

Okay, so if I'm running clean slate,

then when I run this, it's actually gonna delete

the database and then create it again.

So let's see what happens here.

No such file or directory and I think that

we may need to do this in a slightly different way

because this is one of the, you know,

yet another reason why this model container helper

is helpful but then I'm kind of stuck

because I can't really control exactly when this is created.

So if I go back over to base and I try to,

let's take a look at finder.

There's no more application support directory,

so this database is now gone.

It's still there because it ended up,

I guess, moving to trash or something

but we don't have an application support directory anymore.

So if I go back over to songs to play,

now it'll work and then we go back over to finder

and now we have an application support

and so now we have a clean slate.

So this, you know, it's a little bit clunky

and I think I would probably go back to the previous case

where we were setting up the model container ourselves

so that we could do this in exactly

the order that we want to.

Another thing we might wanna do is just, you know,

only remove the store, not the directory

or something like that.

But I think this is a good trick to just have an easy way

to just start over because you're gonna be making changes

and you wanna make sure that the changes you've made

and once you've sort of set into those changes

that you stop making drastic changes over time

because then you're gonna have to deal with migrations.

Okay, so that is a look at the models

in the Swift Data ecosystem and next time we're gonna talk

about how we can start creating these in a SwiftUI app.