- Dec 16, 2024
Unique Macro in Xcode 16 & SwiftUI
Prior to iOS 18, the @Attribute(.unique) property was used to enforce uniqueness constraints in SwiftData models. However, it only supported single-property uniqueness, limiting its use for more complex scenarios.
The newly introduced #Unique macro solves this limitation by allowing compound uniqueness constraints across multiple properties. This means you can specify combinations of properties that must be unique together, rather than just enforcing uniqueness on individual properties.
When using the #Unique macro, SwiftData performs an upsert operation — if a new object has the same unique values as an existing one, the existing object is updated instead of creating a duplicate or triggering an error.
Mastering SwiftData by Example in SwiftUI & iOS 18
Let’s explore these concepts with an example. We’ll create a model to track upcoming trips, which will include the following properties:
Name of the trip
Destination
Start date
End date
We’ll also add a duration property, which will be a computed property. Since it won’t be saved to the data store, we’ll mark it as Transient.
import SwiftData
@Model
final class TripsModel {
var name: String
var destination: String
var startDate: Date
var endDate: Date
init(name: String = "", destination: String = "", startDate: Date = .distantPast, endDate: Date = .distantPast) {
self.name = name
self.destination = destination
self.startDate = startDate
self.endDate = endDate
}
@Transient
var duration: Int {
let calendar = Calendar.current
let components = calendar.dateComponents([.day], from: startDate, to: endDate)
return components.day ?? 0
}
}Let’s add sample data for previews.
extension TripsModel {
@MainActor
static var preview: ModelContainer {
let container = try! ModelContainer(
for: TripsModel.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
)
container.mainContext.insert(
TripsModel(
name: "Summer Trip",
destination: "Paris",
startDate: .now,
endDate: .now.addingTimeInterval(1_000_000)
)
)
container.mainContext.insert(
TripsModel(
name: "Winter Getaway",
destination: "Tokyo",
startDate: .now.addingTimeInterval(2_000_000),
endDate: .now.addingTimeInterval(2_500_000)
)
)
container.mainContext.insert(
TripsModel(
name: "Autumn Retreat",
destination: "New York",
startDate: .now.addingTimeInterval(-1_000_000),
endDate: .now.addingTimeInterval(-500_000)
)
)
container.mainContext.insert(
TripsModel(
name: "Spring Break",
destination: "Miami",
startDate: .now.addingTimeInterval(500_000),
endDate: .now.addingTimeInterval(800_000)
)
)
return container
}
}Enforcing Uniqueness
We want the name of the trip to be unique, so we’ll use @Attribute(.unique) on the name property.
import SwiftData
@Model
final class TripsModel {
@Attribute(.unique)
var name: String
var destination: String
var startDate: Date
var endDate: Date
init(name: String = "", destination: String = "", startDate: Date = .distantPast, endDate: Date = .distantPast) {
self.name = name
self.destination = destination
self.startDate = startDate
self.endDate = endDate
}
@Transient
var duration: Int {
let calendar = Calendar.current
let components = calendar.dateComponents([.day], from: startDate, to: endDate)
return components.day ?? 0
}
}We’ll create a UI to display a list of upcoming trips. Additionally, we’ll add a TextField and a button to allow users to add a new trip with some static data.
import SwiftUI
import SwiftData
struct TripsHomeView: View {
@Environment(\.modelContext) private var context
@State private var newCountry = ""
@Query(sort: \TripsModel.name)
private var trips: [TripsModel]
var body: some View {
NavigationStack {
VStack {
HStack {
TextField("New", text: $newCountry)
.textFieldStyle(.roundedBorder)
Button("➕") {
DispatchQueue.main.async {
context.insert(TripsModel(name: newCountry, startDate: .now, endDate: Date.now.addingTimeInterval(1000000)))
newCountry = ""
try! context.save()
}
}
}
List(trips) { trip in
VStack(alignment: .leading, spacing: 20) {
HStack {
Text(trip.name)
.font(.title3.bold())
Spacer()
Text("\(trip.duration.description) days")
.font(.subheadline.bold())
}
HStack {
Text(trip.destination)
Spacer()
Group {
Text(trip.startDate.formatted(date: .numeric, time: .omitted))
Text("-")
Text(trip.endDate.formatted(date: .numeric, time: .omitted))
}
.font(.caption)
}
}
}
}
.padding()
.navigationTitle("Vacation List")
}
}
}
#Preview {
TripsHomeView()
.modelContainer(TripsModel.preview)
}Similar behavior can be achieved with newly introduced iOS 18 macro called #Unique.
import SwiftUI
import SwiftData
@Model
final class TripsModel {
#Unique<TripsModel>([\.name]) // introduced in iOS 18
var name: String
var destination: String
var startDate: Date
var endDate: Date
init(name: String = "", destination: String = "", startDate: Date = .distantPast, endDate: Date = .distantPast) {
self.name = name
self.destination = destination
self.startDate = startDate
self.endDate = endDate
}
@Transient
var duration: Int {
let calendar = Calendar.current
let components = calendar.dateComponents([.day], from: startDate, to: endDate)
return components.day ?? 0
}
}But this new macro is much more powerful than Attribute(.unique) where multiple properties can create a compound unique entry constraint.
import SwiftUI
import SwiftData
@Model
final class TripsModel {
#Unique<TripsModel>([\.name, \.destination])
var name: String
var destination: String
var startDate: Date
var endDate: Date
init(name: String = "", destination: String = "", startDate: Date = .distantPast, endDate: Date = .distantPast) {
self.name = name
self.destination = destination
self.startDate = startDate
self.endDate = endDate
}
@Transient
var duration: Int {
let calendar = Calendar.current
let components = calendar.dateComponents([.day], from: startDate, to: endDate)
return components.day ?? 0
}
}Let’s update the code to assign a random end date for the newly added sample trip.
import SwiftUI
import SwiftData
struct TripsHomeView: View {
@Environment(\.modelContext) private var context
@State private var newCountry = ""
@Query(sort: \TripsModel.name)
private var trips: [TripsModel]
var body: some View {
NavigationStack {
VStack {
HStack {
TextField("New", text: $newCountry)
.textFieldStyle(.roundedBorder)
Button("➕") {
DispatchQueue.main.async {
context.insert(
TripsModel(
name: newCountry,
startDate: .now,
endDate: Date.now.addingTimeInterval(
TimeInterval(
Int.random(in: 100000...999999)
)
)
)
)
newCountry = ""
try! context.save()
}
}
}
List(trips) { trip in
VStack(alignment: .leading, spacing: 20) {
HStack {
Text(trip.name)
.font(.title3.bold())
Spacer()
Text("\(trip.duration.description) days")
.font(.subheadline.bold())
}
HStack {
Text(trip.destination)
Spacer()
Group {
Text(trip.startDate.formatted(
date: .numeric,
time: .omitted))
Text("-")
Text(trip.endDate.formatted(
date: .numeric,
time: .omitted))
}
.font(.caption)
}
}
}
}
.padding()
.navigationTitle("Vacation List")
}
}
}
#Preview {
TripsHomeView()
.modelContainer(TripsModel.preview)
}Inserting another trip with the same name but an empty destination creates a new entry. However, adding it again updates the existing record since both entries have the same empty destination. You can observe this change through the updated trip duration.
Important Considerations
As of iOS 18 and Xcode 16, the #Unique macro is not compatible with CloudKit synchronization because SwiftData relies on SQLite’s constraint mechanisms.
For relationship attributes, SwiftData only supports unique constraints on references to single persistent models, not arrays of models.
Summary
In this post, we learned how to use the #Unique macro to create powerful compound uniqueness constraints.

