SwiftData

Now that I've upgraded my computer I can use the latest updates in Swift, SwiftUI and the all-new SwiftData introduced last June. I decided to take an old project and update it to use SwiftData to see how it works.
Back in 2016 I made a companion app for an online role-playing game called Clan Lord that fetches the XML feed from the game server, parses it and displays it in an iOS app, along with some astronomical in-game data. I decided to use this real-world example that includes fetching data from the web to test SwiftData.
A few details about the app
So before I go into what I changed to use SwiftData, a few word about what the app does. Every 3 minutes, the server for the game posts an XML feed with various data about the game. The app fetches this XML data and then parses into this ClanLordStatus struct:
struct ClanLordStatus: Codable {
var status: Status
var news: String
var clanners: [OnlineExile]
var announcements: [Announcement]
var poptrend: [Popcount]
}
The status gives the date and time of the last refresh of the XML feed. The news always points to the latest release notes for server updates: Clan Lord what's new, clanners contains the list of currently playing characters, each with the information detailed below, there are four types of announcements which are also detailed below, and finally poptrend is the number of online clanners for each update of the XML for the past few hours, which gives a population trend.
OnlineExile consist of these parameters:
struct OnlineExile: Codable, Identifiable, Hashable {
var name: String
var started: Date
var race: String
var gender: String
var profession: String
var clan: String
var picture: Int
var colors: String
}
With this the app can display this type of information:

And Announcement has these parameters:
struct Announcement: Codable, Identifiable {
var type: String
var time: Date
var messenger: String
var message: String
var awardee: String
}
And the corresponding view within the app:

In addition to these two views, the app also displays various information like the current time, the next sunrise and sunset, etc. Here is that view.1

Adding SwiftData
The information from the XML feed is ephemeral. People come and go, so the list of online exiles varies over time and there is nothing to persist. To test SwiftData I needed some data that would be persisted, so I decided to include the ability to add notes about an exile and also to enable notifications (to get notified when they log in the game).
My first thought was to simply use my old networking code to pull the XML and parse it into a ClanLordStatus struct. I'd have an ephemeral list of OnlineExile and a persistent list of Exile which contains the same information2 plus the notification and notes as well as an online parameter. When I refresh the XML feed I go through the list and update the online status and the app displays exiles that are online.
There are four steps to use SwiftData in an app:
- Add the
@Modelmacro to the model class (it replaces@Observable). - Pass in the
ModelContainerto the view (best done from the@mainstruct so that all your views have access to the model but it's not necessary). - Add data to the model with
.insert(modelObject). - Read the data that you need for the purpose of the view with
@Query.
So in my project I created the Exile model which is similar to the OnlineExile struct but adds the online, notify and notes parameters, and added the @Model macro like this. <- step 1
@Model
class Exile {
@Attribute(.unique)
var name: String
var profession: String
var race: String
var gender: String
var clan: String
var started: Date = Date()
var picture: Int = 0
var colors: String = ""
var online: Bool = false
var notify: Bool = false
var notes: String
}
Then I pass a ModelContainer to the ContentView (don't forget to import SwiftData where needed). <- step 2
import SwiftData
import SwiftUI
@main
struct CL_InformerApp: App {
@State private var networking = Networking()
@State private var astro = AstroVM()
var body: some Scene {
WindowGroup {
ContentView()
.environment(networking)
.environment(astro)
}
.modelContainer(for: Exile.self)
}
}
To add data you need to have access to the ModelContext which represents your model in memory and is managed by SwiftData.
You get access to the ModelContext like this:
@Environment(\.modelContext) var modelContext
Then you add data simply like this. <- step 3
let newPerson = Exile(name: clanner.name, profession: profession, race: race, gender: clanner.gender, clan: clanner.clan)
modelContext.insert(newPerson)
And then when you need to access the data from the model all you need is to read the data with a query which can be configured with predicates and sorting options. So something like this for example. <- step 4
@Query private var people: [Exile]
...
NavigationStack {
List {
ForEach(people) { person in
NavigationLink(value: person) {
Text(person.name)
}
}
}
...
}
That's all it takes to use SwiftData! This is what the view for an 'exile' looks like 3:

Current status
So far so good! Of course, there's a lot more to SwiftData than just that. This is only a first order implementation to get things working and it works pretty well as far as displaying who is online and persisting the notes. I'm also quite happy with the way the app looks (both in light and dark mode) and how the astronomical information is presented.
The issues I have is that sometimes the app isn't very responsive, and more importantly the notifications don't work. I think the performance issues can be resolved by refactoring the code to use async tasks so that changes to the model occur more smoothly. For notifications, I think I need to look into ModelActor, and that'll be the subject of another post. Stay tuned!
IRL means In Real Life, as opposed to CL for the Clan Lord gameworld. When it's 3:30 PM in CL time, it's currently 11:19 PM in real life. But it won't be the case tomorrow. Times flows 4 times faster in the game world than in real life, so it's always out of sync from ours and without tools like this it's impossible to know the exact in-game time.
↩
picture parameter which gives an ID and colors parameter. This is included as part of the XML information. I just need to figure out how to get the picture from the CL Images file and then how to swap the specific exile's colors with the placeholders in the image.
↩
