Understanding SwiftUI persistency wrappers through their procedural alternative
If you just started your journey with SwiftUI you must surely have seen stuff like @State, @StateObject, @AppStorage, @FetchRequest, etc. All of those are property wrappers. I highly suggest reading the Swift Documentation to know in depth what a property wrapper is, but it isn’t needed to read this guide. We will talk about @AppStorage and @FetchRequest, those are two wrappers that can read and save data from the stored memory, to understand them we’ll explore their procedural alternative to understand how they work under the hood.
What does “Procedural alternative” mean?
If you have ever programmed in other languages you know that if you want to get the program to draw something you will have to tell it something like:
PutBrushAtPosition(200,200)
DrawLineTo(400,200)
DrawLineTo(300,400)
DrawLineTo(200,200)
AddSquareAt(500,500)
FillCircleAt(200,200)
PutTextIntCircleAt(200,200,"I'm a circle")
You are writing a list of commands to be executed in order: a procedure. When you program something like that you have full control of the program but sometimes you risk forgetting what your code does.
Canvas {
Line(form: (200, 200), to: (400,200))
Line(form: (400, 200), to: (300,400))
Line(form: (300, 400), to: (200,200))
Square(at: (500, 500))
Circle(at: (200, 200))
.fill()
.innerText("I'm a circle")
}
In this example instead, we have a declarative way to write an equivalent program. The difference seems subtle but it is substantial. In the previous snippet, we are giving the PC the recipe to create the image that we want to show on the screen, in the second one we are only describing what we want to see without caring how it is calculated.
@AppStorage and UserDefaults
What are UserDefaults?
UserDefaults are a really simple and useful way to quickly save a small amount of data on the storage. They are structured like a dictionary, you can save and load data using a key. For example, you can store the integer 10000
with the key "score"
so that when you will ask for the value with that key UserDefaults will retrieve it until you change it. Let’s see how they work:
UserDefaults.standard.set(10000, forKey: "score")
let score = UserDefaults.standard.integer(forKey: "score")
This is the code for saving and loading 10000
with the key "score"
. Let’s analyze a bit:
UserDefaults
is the class we interact with to use UserDefaults;standard
is an instance of UserDefaults. UserDefaults allows you to create your instances but for 99.9% of cases it is not needed;set
is the method by which you save all the data (integers, strings, raw binary…);integer
is the method by which you load integers. One such method exists for each type (ex.:string
for strings,data
for raw binary…).
Ok all very cool, but what does it matter with AppStorage?
What is AppStorage?
AppStorage like any good property wrapper wraps a property… but what does that mean? A wrapper customizes access to a variable by running custom code each time you want to read or write the wrapped property.
Let’s look at these two snippets:
var score: Int {
get {
UserDefaults.standard.integer(forKey: "score")
}
nonmutating set {
UserDefaults.standard.set(newValue, forKey: "score")
}
}
@AppStorage("score") var score: Int = 0
These do almost the same thing. The exciting part in the second case is not so much that we are using less code, but that the code we are using is no longer a recipe for how to save the value in storage, rather we are declaring that this value is saved in storage without worrying about how. It is not a matter of saving time but of readability.
How to use AppStorage
Now that we understand how it works, let’s see how to use it.
@AppStorage("score") var score: Int = 0
1 2 3
"score"
is the key we want to access;Int
is the type of data we want to access;0
is the default value for the variable that is automatically initialized with that value if nothing has been saved with this key yet.
And that’s it! There is not much to add, these are the notions that will suffice for you in the vast majority of cases.
@FetchRequest and CoreData
CoreData is a powerful tool that allows you to store a large amount of data in tables. In this guide, I'll not explain much about CoreData because it is a huge topic, and I’m not qualified to speak about it. Instead, we will focus on an initial setup and how to integrate it into our SwiftUI apps.
How CoreData works
To put it simply, CoreData allows you to create tables called entities that represent the type of data that you want to save. To create these entities easily in your Xcode project, you can create a new file and choose as file type Data Model. To create a new entity you can find at the bottom slightly to the left a button called Add Entity, clicking on it you can see a new record in the sidebar called Entity. You can click on it and change his name how you want, I called it User. Selecting it now you can see three tables that you can fill. Today, we’ll speak only about the first one: attributes. In this table, you can add new attributes that will describe which simple data will compose the complex ones. To add one you can tap the plus button under the table and choose name and type. In our case, since I choose to describe a user I’ll add two attributes: name of type String and passcode of type Integer 16.
By doing something like that we are creating a single table that looks like that:
So how can I use it? To use this table you have to create an instance of NSPersistentContainer
passing to it the name of the file containing the entities:
let container = NSPersistentContainer(name: "Model")
To keep it simple this object wraps up all the persistence stuff that you need to interact with the storage. Inside it, there is a property called viewContext
this object tracks all the interactions with the data and this is what we need to write and read on the persistent memory.
let context = container.viewContext
Now to save our first object we can do this:
let user = User(context: context)
user.name = "Ugo"
user.passcode = 2222
try? context.save()
When we create an instance of User passing the context, CoreData automatically sets this object as a new element of our table, and after filled we can save the context and this we’ll be saved on the storage. To retrieve all the data saved in the table as a list we’ll use the fetch method:
try? context.fetch(NSFetchRequest(entityName: "User")) as? [User]
To edit an element of the table you can modify an element of the list and save the context:
let users = try? context.fetch(NSFetchRequest(entityName: "User")) as? [User]
let aUser = users?.first
aUser?.name = "Flavio"
try? context.save()
Instead, to delete it you can use the delete
method of the context:
context.delete(aUser)
try? context.save()
I know, there are a lot of things that seem to have no sense in this chapter but the reality is that CoreData is a really powerful tool that should be used for something way more complex than that.
At this point, I highly recommend doing some tests on your own to better understand how it works. For example, try to add some attributes or display data in SwiftUI.
How @FetchRequest works
In contrast to how AppStorage works, FetchRequest doesn’t write on the storage but reads only from it. Let’s make a comparison also here between FetchRequest and its procedural alternative:
@Environment(\.managedObjectContext) private var viewContext
var users: [User]? {
return try? context.fetch(NSFetchRequest(entityName: "User")) as? [User]
}
@FetchRequest(fetchRequest: NSFetchRequest(entityName: "User"))
var users: FetchedResults<User>
Also here we have two snippets that have a similar result. As you can see in the first one you have to retrieve the environment value managedObjectContext because that is exactly what FetchRequest does, so is a good practice to pass it as soon as possible in the hierarchy of the views. Here we have an example:
import SwiftUI
import CoreData
@main
struct TutorialApp: App {
let container = NSPersistentContainer(name: "Model")
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, container.viewContext)
}
}
}
For now, it’s all. I hope that this guide helped you to better understand the behaviors of SwiftUI under the hood. See you soon in the next guide!