SwiftData ❤️ SwiftUI - Part 1
I have been a strong advocate of UIKit (Still is) due to the level of control we get but it seems Apple has different things in mind. With the introduction of VisionOS, it’s clear that SwiftUI is going to be the future for not only iOS but all Apple devices. The thing that always stopped me from using SwiftUI was the performance. I used to think will SwiftUI ever be as efficient as UIKit? I have noticed that since iOS 14+ it is.
What are we building?
For me, it’s hard to remember dates. Yes, you can set a reminder of that date but that reminder fails to create a sense of urgency. Let’s create one app where we can add events that happen in future. It would be great if we could see all upcoming events on the main app. Maybe add them to the widget to create a sense of urgency.
For any event what are the things we need to keep track of?
- Name of the event e.g. Mom’s Birthday
- Date
We will add more properties in the upcoming article where we will discuss migration. For now, that’s all we need. Let’s start
App Interface
If you are only interested in SwiftData, feel free to skip this section.
Let’s create a very simple interface for showing a list of events. Add a plus button which will open a sheet where we can add a new event. Upon saving it we update our main list.
Main List
Our list will show a list of events. Each event has a name and date. So, Let’s create a model to represent our event object.
struct Event {
let name: String
let date: Date
}We can create a very basic list with the given code:
struct ContentView: View {
var events: [Event] = [.init(name: "Happy Birthday!!", date: .init(timeIntervalSinceNow: 60 * 24 * 24))]
var body: some View {
VStack {
ForEach(events, id: \.self) {
Text($0.name)
}
}
}
}If you have followed me so far, you should be getting an error. This is because for using ForEach our data should be hashable i.e. we have to make our event model conform to hashable.
Updated Model:
struct Event: Hashable {
let name: String
let date: Date
}Let’s start creating our individual cell views. We want to show the event name on the left side then spacing then the time at which that event is due. We will have one button to Add a new event below the list.
struct ContentView: View {
var events: [Event] = [.init(name: "Happy Birthday!!", date: .init(timeIntervalSinceNow: 60 * 24 * 24)), .init(name: "Important Meeting", date: .init())]
var body: some View {
VStack {
ForEach(events, id: \.self) {
RowView(event: $0)
}
Spacer()
Button(action: {
let impactGenerator = UIImpactFeedbackGenerator(style: .medium)
impactGenerator.impactOccurred()
}, label: {
Text("Add Event")
.frame(maxWidth: .infinity)
})
.buttonStyle(CustomButton())
.padding(.horizontal, 32)
}
}
}
struct CustomButton: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding()
.background(.blue)
.foregroundStyle(.white)
.clipShape(.capsule)
.scaleEffect(configuration.isPressed ? 1.1 : 1)
.animation(.easeInOut(duration: 0.2), value: configuration.isPressed)
}
}
struct RowView: View {
let event: Event
var body: some View {
HStack {
Text(event.name)
Spacer()
Text(event.date.formatted())
}.padding(16)
}
}
#Preview {
ContentView()
}Add Event Flow
Let’s start creating flow for Add Event. For now, we just want to ask the user to enter the event name, event date and time and that’s it.
struct ContentView: View {
@State var showAddEvent = false
var events: [Event] = [.init(name: "Happy Birthday!!", date: .init(timeIntervalSinceNow: 60 * 24 * 24)), .init(name: "Important Meeting", date: .init())]
var body: some View {
VStack {
ForEach(events, id: \.self) {
RowView(event: $0)
}
Spacer()
Button(action: {
let impactGenerator = UIImpactFeedbackGenerator(style: .medium)
impactGenerator.impactOccurred()
showAddEvent()
}, label: {
Text("Add Event")
.frame(maxWidth: .infinity)
}).sheet(isPresented: $showAddEvent, content: {
AddEvent()
})
.buttonStyle(CustomButton())
.padding(.horizontal, 32)
}
}
}struct AddEvent: View {
@State var eventName: String = ""
@State var eventDate: Date = .now
var body: some View {
VStack(spacing: 16) {
Text("Add Event")
.font(.title)
TextField("Event Name", text: $eventName)
HStack {
Image(systemName: "calendar")
Text("Date")
Spacer()
DatePicker("", selection: $eventDate, displayedComponents: .date)
}
Spacer()
Button(action: {
let impactGenerator = UIImpactFeedbackGenerator(style: .medium)
impactGenerator.impactOccurred()
}, label: {
Text("Add Event")
.frame(maxWidth: .infinity)
})
.buttonStyle(CustomButton())
.padding(.horizontal, 32)
}
.padding()
}
}Now, our basic UI is set up. Let’s focus on the data layer.
Data Layer using SwiftData
We have already defined the model. Now we need to write some 100 lines of code to create. Nope, was kidding. Just add @Model macro and we are done.
@Model
class Event: Hashable {
let name: String
let date: Date
init(name: String, date: Date) {
self.name = name
self.date = date
}
}That’s it. @Model is equivalent to schema creation in core data. We had to change struct to class because struct can not conform to PersistentModel protocol.
We can add further metadata to configure our model e.g. if we want the event name to be unique we can simply write:
@Attribute(.unique) let name: StringSimilarly, @Relationship is used to define the relationship between related objects e.g. if we had guests
@Model
class Event: Hashable {
let name: String
let date: Date
let guests: [Guest]
....
}then once the event is removed we want to remove all the guests from persistence as well. This can be done as
@Relationship(.cascade) let guests: [Guest]For excluding property i.e. not save property in persistence use @Transient.
Model Container: This is what provides persistence to us. What does that mean? It means this is the place where data is saved. How do we use it? Let’s see:
if I have a view where I want to read or update SwiftData then we can do it by using modelContainer viewModifier.
struct ContentView: View {
.......
.modelContainer(for: Event.self)
}
}Model Context: This is responsible for tracking updates, fetching models, saving changes and undoing changes. When we are using modelContainer we can get modelContext from the environment. It will be more clear once we start using it.
Read all events from SwiftData
For reading all events, we can use @Query.
struct ContentView: View {
@Query var events: [Event]
@Environment(\.modelContext) var modelContext
@State var showAddEvent = false
var body: some View {
VStack {
ForEach(events, id: \.self) {
RowView(event: $0)
}
Spacer()
Button(action: {
let impactGenerator = UIImpactFeedbackGenerator(style: .medium)
impactGenerator.impactOccurred()
showAddEvent.toggle()
}, label: {
Text("Add Event")
.frame(maxWidth: .infinity)
}).sheet(isPresented: $showAddEvent, content: {
AddEvent()
})
.buttonStyle(CustomButton())
.padding(.horizontal, 32)
}
.modelContainer(for: [Event.self])
}
}That’s it. As new events are added or removed this list will update automatically.
Write new events to SwiftData
For our application, we are using only one Model Container. Let’s add this to our main scene and remove it from other places.
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Event.self)
}Now adding an event is as easy as it can get. Just use:
modelContext.insert(object: event)
That’s it, folks. SwiftData is still in beta and this article might need changes in future. Feel free to reach out to me in case you have any queries or suggestions. See Ya!!




