avatarRajesh Budhiraja

Summary

The article "SwiftData ❤️ SwiftUI - Part 1" provides a guide on building a SwiftUI-based application with a focus on integrating SwiftData for data persistence, emphasizing the ease of use and efficiency of SwiftData in modern iOS development.

Abstract

The author of "SwiftData ❤️ SwiftUI - Part 1" begins by acknowledging their preference for UIKit due to its control but recognizes the shift towards SwiftUI, especially with the introduction of VisionOS. The article addresses past performance concerns with SwiftUI, suggesting improvements since iOS 14+. The tutorial aims to create an app that helps remember future events by displaying them in a main list and potentially on a widget to instill a sense of urgency. The author outlines the necessary SwiftUI interface components, including a model for events, a main list view, and an add event flow. The article delves into the data layer using SwiftData, demonstrating how to define models with the @Model macro, handle relationships with @Relationship, and exclude properties from persistence with @Transient. It also explains the use of @Query for reading data and modelContext.insert for writing data. The author concludes by simplifying the data persistence process with SwiftData, making it clear that SwiftUI and SwiftData are the future of iOS app development.

Opinions

  • The author is a strong advocate for UIKit but acknowledges the industry's shift towards SwiftUI.
  • There is an opinion that SwiftUI's performance has significantly improved with recent iOS updates.
  • The author believes that creating a sense of urgency for events is important and can be achieved through a well-designed app interface.
  • SwiftData is presented as a powerful and straightforward tool for data persistence in SwiftUI applications, reducing the need for extensive coding.
  • The author expresses enthusiasm about the future of SwiftUI and SwiftData, suggesting that they will be integral to iOS app development.
  • The article implies that SwiftData's simplicity and functionality make it a compelling alternative to Core Data for SwiftUI developers.

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?

  1. Name of the event e.g. Mom’s Birthday
  2. 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: String

Similarly, @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!!

Swift
Swiftdata
Swiftui
iOS Development
iOS
Recommended from ReadMedium