2026-02-10 • 29:05
Learn the tools and techniques for managing data flow in SwiftUI. Discover how to model your data with property wrappers, leverage the power of @Observable, and implement persistence in your app.
This session was originally presented as part of the Meet with Apple activity "SwiftUI foundations: Build great apps with SwiftUI”. Watch the full video for more insights and related sessions.
Speaker: Cole Imhoff
Part of: SwiftUI foundations: Build great apps with SwiftUI
Downloads from Apple
Transcript
Good afternoon everyone. My name is Cole and I’m a core technology evangelist here at Apple. And today I’m going to discuss data in your app and how to flow it through to your SwiftUI views. The Wish List app is a great example that I’ll use throughout the day to demonstrate some of the ways that you might encounter data flow in SwiftUI apps. The Wish List app does a lot of the same things with its data that your apps may need, and it takes different approaches depending on the type of data in question.
I’ll explain some of the most important ones first. There are certain places in this app where the state of the interface will change when someone interacts with it. For example, when someone’s modifying a trip, the app will need to keep track of whether the UI is in editing mode or not. Now there’s a few different places where this will come up, like keeping track of when a sheet is shown after tapping a button, or when an alert needs to be displayed. I’m generally going to refer to these cases as Viewstate.
Now, there’s also a data model in this app. The goal of the Wish List app is to show all of this rich data from my brilliant trips and activities. For example, this trip to Kyoto shows a bunch of fun activities to do while there like exploring temples, walking the canal paths at sunrise, and more. So I’m going to need to model this data in the app and flow it through to all of my SwiftUI views.
The app will also need to keep track of preferences that someone sets in the app. For example, the app has a sort button on the list of activities on a trip. You can sort by title, by date, or whether the activities are completed, the app will need to keep track of the last sorting method that someone chose so that it uses that same sorting method the next time this view is shown.
And finally, I want to build an app or sorry, I want to build an approach for persistence. Persistence is an approach that lets an app save its data to the device’s storage, like in a file. That way, the next time someone comes back to the app, all of their changes are still there, even across relaunches of the app.
So in this session, I’m going to discuss each of these use cases more in detail. This includes data for view state, building a rich data model, handling preferences and configuration, and a few techniques for persistence. By the end of this session, you’ll be equipped to reason about each of these use cases, and you’ll know patterns that you can replicate to address similar needs in your own apps. So first I’m going to start with Viewstate.
As I mentioned earlier, sometimes an app just needs to create a piece of data to track the state of the interface itself. These things are often temporary and safe to reset if the view disappears. For example, the sample app tracks whether or not a trip is in editing mode or not. If someone navigates away from this trip and then comes back to it later, it makes sense that the UI is no longer in editing mode. Even if I didn’t tap done.
Here’s another example of state that needs to be tracked in the interface. In this app on the wishlist tab, there’s an add button in the toolbar. Now, Kurt talked about this button earlier when refining animations. Tapping this button makes a sheet appear where someone can start entering the details of a new trip to add to the app.
So the app will need a piece of state to track whether or not the sheet is currently shown on on on the screen. Also, the sheet itself has buttons to save changes or cancel adding a trip. So those actions in the sheet will need a way to update the state so that SwiftUI will dismiss the sheet.
So here’s how this add button is implemented in SwiftUI with the sheet in the body of wishlist view. There’s a SwiftUI button with a symbol label, and I’ll need to add some code to the button action to show the sheet to present the sheet itself. I need to use the sheet Ispresented modifier, which is added to the view.
Now, before I can complete this code, SwiftUI needs me to make a decision. What is the data that tracks whether or not this sheet is actually shown on the screen right now? Well, this is a great use case for using the at state API. Here’s how it works. I’ll create a new property called is presenting add trip and decorate it using at state and it defaults to false.
Using at state. Tells SwiftUI to create a new piece of data that lasts as long as this view does. I’ll actually say more about the lifetime of this value in a bit, but for now, now that I’ve defined that, I can use it in the rest of my view.
In the button action I’ll set is presenting add trip to true, since tapping the button should always make the sheet appear, and I’ll disable the button if the sheet is already being presented by passing the state to the disabled modifier. Note that because the view body reads the. Is presenting add trip value like in this modifier. The wishlist view establishes a dependency on this value anytime. Is presenting add trip changes. SwiftUI will update this view.
I’ll also pass the is presenting Add Trip property to the sheet modifier. This dollar sign notation allows me to pass a binding to this state. Letting this code share this value with the sheet so it can both read and make changes to it. I’ll come back to explain more about bindings shortly.
Decorating a property with at state tells SwiftUI that this property is owned by the enclosing view. The value of this property exists as long as the view does, and this is an important point that I just touched on earlier at. State properties match the lifetime of the view in the interface, not the lifetime of the view struct itself. So I’ll dig more into how this actually works.
As Leah discussed earlier today, SwiftUI views are just short lived descriptions, like a template. When the wish list view appears in the interface, SwiftUI runs its body in order to update the UI on the screen, but then it throws the instance away, and this repeats every time SwiftUI renders the view.
So why doesn’t the is presenting add trip property get reset anytime this view gets reinitialized since the default value is false? Well, that’s because SwiftUI gives special treatment to at state properties like this one. Behind the scenes, SwiftUI has its own data storage for all of the state owned by views. When SwiftUI instantiates this view for the first time, it allocates space in its internal storage for this property alongside all of the state owned by other views. This storage is indexed by the view’s identity.
Think of the identity as corresponding to the lifetime of this view’s rendered results on the screen. So the first time SwiftUI renders the view, it assigns it an identity. And when someone navigates away from this view, SwiftUI deletes the entry from its storage. If they come back to the view later, SwiftUI assigns a new identity and allocates a new entry in its storage.
So here’s how this will work with my button that will show a sheet when tapped the first time someone navigates to a wish list view. SwiftUI allocates storage for. This views is presenting add trip state property using the default value of false. Then it instantiates the struct and sets the value of. Is presenting. Add trip to match Swiftui’s internal storage and then runs wishlist View’s body to render to the screen, and then it discards the view instance when the person using the app then taps the button.
The action closure runs setting is presenting add trip to true, but because this is an at state property, this assignment modifies Swiftui’s internal storage and because the body of the view depends on this state, SwiftUI triggers an update. Creating a new view struct populating the value of is presenting add trip with the new true value from internal storage. Before running body.
So this internal storage is how SwiftUI maintains state for the lifetime of the view in the interface. Even though the structs themselves are transient. Okay, so this internal storage explains how this at state property asks SwiftUI to track a boolean for the lifetime of this view in the interface. But at state isn’t just useful within a single view declaration. It can also be shared with other views using something called a binding. This dollar sign in my sheet modifier allows me to access a binding to. This is presenting add trip state.
A binding is just a read write reference to some piece of state in your app. They’re great when you’re creating a view that needs to read a value from its enclosing view, and might need to change that value to. Bindings are used all over SwiftUI. You’ll find them in the parameters for toggles and text fields and the sheet modifier and more. These are encapsulated controls that depend on this data and can modify it when needed.
For example, here’s a snippet of the Add trip view, which is the view inside the sheet itself. It uses at binding to receive a reference from the enclosing view to the state that tracks whether the sheet is presented in the view body. There are buttons that can cause the sheet to be dismissed. In order to do this, they set the value of the Ispresented binding to false in their action closure. That causes SwiftUI to update all of the views that depend on the state like mine. Closing view and then leads to the sheet being dismissed.
Places like my sheet where I just want to track when someone interacts with the interface is a great fit for. at state. It’s temporary data that should reset when the view goes away, or when you have objects that should only exist as long as the view does. But at state isn’t the best fit for other types of data, like my data model. For those cases, you’ll want a way to express types of state to SwiftUI that’s owned by other parts of your code, not owned by the UI itself.
So that leads me to my next use case, which is my data model in wishlist. The data model is the heart and soul of this app. It includes all of the information that people want to come back to the app for, like the names and photos and activities on each trip.
Like in this example, the trip object has the name Peru off Road and has this gorgeous photo associated with it. There are also relationships between these objects. For example, a trip can have multiple activities and activities, have their own attributes like a name and a boolean to track whether the activity is complete.
So I’ve created a data model using classes to represent trips and activities in this app. Each class has properties to store attributes like name, photos, and dates. They also store references to each other in order to model the relationship between these objects. For example, the trip object can have any number of activities associated with it by storing references to them in the activities property. And these objects are also editable, as people might change these properties in the UI. Like when someone edits the name of a trip in a text field.
I’ve also created a class called DataSource to manage all of these objects in the app. It’s a single place where the app can look up all of the objects that it needs to show in the UI, like getting a list of all of the trips. So having a data model like this is a great start. But to really use this data model in SwiftUI, I do need to add one quick thing. The data model needs to be able to know when changes to its properties happen, so that SwiftUI can update the UI.
And this is what the observable macro is for. With this one line of code, you can give SwiftUI the power to establish dependencies on the properties in your classes. The observable macro adds update tracking to each of the classes properties. To use it, just decorate each class with the at observable macro.
So now that the data model is observable, SwiftUI can now establish dependencies on model properties directly. In this example, the trip card view takes a reference to the trip object it should display. Note that there’s no at state or at binding. In fact, there’s no decoration at all on this reference. What makes this view work is that inside the view body it reads the trips photo URL property. This tells SwiftUI that the trip card needs to update anytime the photo URL property changes on this observable class.
And this also works through computed properties. For example, let’s say I want to use a placeholder photo for trips that don’t have a photo saved yet. to handle this in the UI. I could define a computed property on trip called photo URL or placeholder. This computed property returns a placeholder photo if there isn’t one defined for the trip. And then I use that new computed property in the view body instead of the photo URL. SwiftUI is able to determine that the underlying property photo URL is read in this view body when the computed property is accessed. So SwiftUI will update trip card any time the underlying photo URL is changed.
Okay, so to recap, this app uses a structured data model of trip and activity objects. It uses reference types or classes to model each object, so that I can use references to model the relationship between these objects, and it’s easy for these properties to be editable anywhere in the UI. To make data in these classes flow through to SwiftUI, I just add the observable macro to each of their definitions. Then my SwiftUI views can just read the properties directly in view bodies.
Now there’s one more thing I need to do. What I’ve shown so far works great. Once a view already has a reference to an object like a trip. Now, I said earlier that all of these objects are owned by the data source class, but how do I tell my views which data source instance to use?
Well, I’ve actually already discussed one of the ways that you can do this, and it starts with at state. Recall that at state we’ll create an object that exists as long as the view does so at the top of my app, I could create an at state property that holds a data source object. Putting at state in this app declaration creates an object that exists as long as the app does, and then it can be passed down to the views.
Now, this might work well for very simple view hierarchies. In this app. Different parts of the UI may need to get access to the data source. For example, to get the full list of trips and with a very simple view hierarchy, it’s perfectly reasonable to just pass an object like data source down to the views that need it.
But as an app grows in complexity, this approach might start to become cumbersome. An app with many nested layers of views may end up needing to pass and store a reference to the data source, just so it’s able to pass it along to the deeper views that need it.
So to avoid this problem, cases like this can be a good fit for the environment. Think of the environment as the essential properties which configure parts of your app, and they don’t change very often. In this case, I’ll create a data source object as state and I’ll set that object on the views environment. This configures the views that need a data source with this particular instance.
This is safe because the data source object itself won’t be swapped out very often. It’s observable and there are lots of different views in my app that may need a reference to it. So by putting the data source in the environment, any view that needs a reference to it can just grab one directly.
The environment flows through the entire view hierarchy that it’s applied to of you just has to ask for a value from the environment and SwiftUI will provide it. For example, deeper in the view hierarchy, the recent trips page view gets a reference to the data source by decorating a property with.
At environment, SwiftUI will populate the value of this reference with the object of a matching type from the environment. Since data source is observable, a view can then establish dependencies on its properties right from within the view body. Here, the recently added Trips property is read in this view body, so the recent trips page view will update anytime a new trip is added to the app.
So view state and my data model cover a lot of the data flow that I need in this app so far, but there’s a couple of other use cases that take a slightly different approach. And so I’ll continue with talking about preferences and configuration in this app. I mentioned earlier that when someone taps on a trip, they can sort the list of activities that appear by name or by whether they’re completed.
The app will need to store this preference so that anytime someone comes to this view, it uses their preferred sorting method. Now, preferences aren’t a great fit to go inside the data model that I discussed earlier, because this doesn’t really have anything to do with the trips themselves. It’s something that tracks how people prefer to use different features of the app.
So to build this sorting feature, I could start by defining a new piece of state using at state to track what sorting method is being used. In this example it’s called sort option. And then the activity. Subview will read the sort option when laying out its activities. Now this works and the activities are now sorted by what someone chose in the menu.
But recall that at state properties like this one are only stored for the lifetime of the view. So if someone leaves this view and then comes back to it, the sorting will always revert to the default, which is by title. I want to make it so that anytime someone comes back to the trip detail view, it’s sorted in the same way they selected before.
So to do this, I’ll change at state to at app storage and provide a unique identifier for the setting. App storage works really similarly to at state. It declares a piece of state that is global to your app, and it works best with simple codable types. App storage values actually get stored to disk automatically under the hood. It uses an API on Apple platforms called Userdefaults, which stores a set of keys and values for your app. And this is a great place for settings, preferences and app configuration details.
So now by by saving the sorting preference in an app storage property, the activities. Now always sort by the last option that someone chose in the view, even as they navigate across many different trips. Finally, the last use case that I’d like to discuss today is persistence. The example I just showed you where I saved that sort preference by using app storage is actually an example of persistence. SwiftUI will automatically use the Userdefaults API to get the value of this key on disk, and then set the value in the view to match.
And if the sort option is assigned a different value, app storage will save that new value back to Userdefaults. So even if someone were to come back to your app a day later, or a week later or a month later, the sort option property will remain the same until it’s changed.
But of course, it’s not just preferences that you might want to persist. This app has this rich data model, and people are definitely going to want to keep their wish list saved for days or weeks or more. So to build a data model like this with persistence in your app, one great option is Swift data.
Swift data is a framework that lets you add persistence to your app quickly, with minimal code and no external dependencies. It uses modern Swift language features like macros and property wrappers, and that lets you describe your models just by writing Swift code. By default, it leverages Core Data’s proven persistence. Technology and models in Swift data are all automatically observable. To learn more about Swift data, check out the videos, meet Swift data, and build an app with SwiftData.
As I shared earlier, the data model is comprised of a few classes, and each of its attributes and relationships to each other are stored as properties on each of these classes. And currently, these classes are decorated with the observable macro. But if I wanted to make them persist using Swift data, I would just replace the observable macro with the model macro. The model macro turns these classes into Swift data models. And again, this automatically makes them observable as well.
So as a fun exercise, you can try downloading the sample code when it’s available and going through a few steps to migrate this app over to SwiftData. For those of you interested in doing that, I’ll explain a few of the key things you’d want to change in the code. First, you’ll want to add some metadata to your model. You’ll refine how data is fetched or queried from inside the app’s views, and you’ll set up a container to store your models in. So I’ll touch on each of these steps quickly.
First, after switching the model from Observable to App model, you’ll go through the models and annotate properties where you’d like to give Swift data more information about them. For example, in the trip model, you can use at relationship on the activities property to define the relationship between trips and activities.
Here I set the delete rule to cascade so that when a trip is deleted, all of its activities are also deleted. I’ve also set the inverse relationship on the trip property to sorry, the inverse relationship of the trip property on activity. So this means that the trip property on these activities will always point back to the trip that it belongs to.
Now, Swift data makes it super easy to fetch and display models within a view. By using at query, you can just ask Swift data for an array of models sorted and filtered the way you want. In this snippet, I’ve updated the recent trips page view I showed earlier. Now, it used to use the data source object to get a list of recently added trips, But with swift data I can just use at query instead.
This query asks Swift data to provide an array of trips sorted in descending order by creation date. The view body then uses this recently added trips array in a. For each. Swift data will update this view if the query result changes like when a new trip is added to the view.
And finally, in your app declaration, you’ll configure your app with a container to store models in by adding the model container modifier and passing the names of your model types. The app will use a default container for its models, so these three changes. Adding a property metadata, refining queries, and then adding a model container are the keys to get started. And this brings SwiftData’s powerful persistence to an app like this one.
So now I’ve covered all of the data flow use cases for the wish list app, and you’re empowered to take similar approaches in your SwiftUI apps as well. When a view in your app just needs to track simple UI state, like when a button was tapped, use at state and then use bindings to let other views or controls share that piece of state.
Consider using the observable macro for rich data models that you’re building with reference types, like classes. When you want to save data. So save data to storage so that it sticks around. Use app storage for small things like settings and preferences. And consider the Swift data framework for your model data. Thank you for your time. Hope you enjoy the rest of your event. And back to Leah.