Overview
In the latest WWDC 2024, Apple has made major feature upgrades to multiple system frameworks. How can this be done without SwiftData, the “rising star”?
Vientiane’s updated iOS 18 adds new uniqueness, custom data warehousing, rich expressions, and field indexing to SwiftData.
In this blog post, you will learn:
- Overview
- 1. What is SwiftData?
- 2. New #Unique Macros
- 3. Historical data operation record
- 4. Custom Data Warehouse(Data Stores)
- 5. Xcode preview Traits
- 6. Customize Extra Data Queries
- 7. #Expression Expression Macros
- 8. #Index Macros
- Summary
With less gossip, let’s jump into SwiftData’s new world together!
Let’s go!!! 😉
1. What is SwiftData?
Last year, Apple took the iOS 17’s Dongfeng to the new pure Swifty Fan database SwiftData.
With SwiftData, we can easily build a “full-fledged” database support application with only a few lines of descriptive code.
SwiftData has built-in features that we can use to build complex database apps that are local or supported by iCloud.
As shown in the following code, we use @Model macros to convert bland Trip, BucketListItem, and LivingAccommodation classes into SwiftData types that are “supported” behind persistent storage:
// Trip Models decorated with @Model
import Foundation
import SwiftData
@Model
class Trip {
var name: String
var destination: String
var startDate: Date
var endDate: Date
var bucketList: [BucketListItem] = [BucketListItem]()
var livingAccommodation: LivingAccommodation?
}
@Model
class BucketListItem {...}
@Model
class LivingAccommodation {...}
With the above type definition, we can naturally incorporate data into the SwiftUI view:
// Trip App using modelContainer Scene modifier
import SwiftUI
import SwiftData
@main
struct TripsApp: App {
var body: some Scene {
WindowGroup {
ContentView
}
.modelContainer(for: Trip.self)
}
}
In addition, with the help of the new macro mechanism in Swift 5.9, we can arbitrarily set and adjust various properties and relationships in the SwiftData type. For example, we can use the @Transient modifier so that the corresponding attribute in the class does not occupy the field space of the database but only remains in memory.
2. New #Unique Macros
Starting from iOS 18, Apple added a new #Unique macro to SwiftData to represent the uniqueness of attributes in the type, and changed the new operation to the data update operation when the newly inserted object conflicts with an existing object (Collisions):
In the following code, we use the #Unique macro to combine the values of the name, startDate, and endDate attributes in the Trip class to determine the uniqueness of the Trip in the database:
// Add unique constraints to avoid duplication
import SwiftData
@Model
class Trip {
#Unique<Trip>([\.name, \.startDate, \.endDate])
var name: String
var destination: String
var startDate: Date
var endDate: Date
var bucketList: [BucketListItem] = [BucketListItem]()
var livingAccommodation: LivingAccommodation?
}
This allows us to eliminate a lot of additional judgment code and check the uniqueness of managed objects at the database level with #Unique macros.
3. Historical data operation record
With the above #Unique macro, we can further add history (History) record support for SwiftData type instance attributes.
As shown in the following code, we use the @Attribute macro to enable the .preserveValueOnDeletion feature on the three attributes corresponding to the #Unique:
// Add .preserveValueOnDeletion to capture unique columns
import SwiftData
@Model
class Trip {
#Unique<Trip>([\.name, \.startDate, \.endDate])
@Attribute(.preserveValueOnDeletion)
var name: String
var destination: String
@Attribute(.preserveValueOnDeletion)
var startDate: Date
@Attribute(.preserveValueOnDeletion)
var endDate: Date
var bucketList: [BucketListItem] = [BucketListItem]()
var livingAccommodation: LivingAccommodation?
}
In this way, we change the above three attributes to tombstone attributes, so that they can be displayed in the history record when the Trip managed object is deleted, so that the user can view the “transition history” of the database directly.
4. Custom Data Warehouse(Data Stores)
In the previous SwiftData, we could create a corresponding model container for a particular managed type in SwiftUI with the .modelContainer modifier:
// Trip App using modelContainer Scene modifier
import SwiftUI
import SwiftData
@main
struct TripsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Trip.self)
}
}
We can also create “what we want” complex model containers based on our needs, such as the Model Container container we created in the following code to keep all Trip managed objects in memory only, and turn on autosave and Undo:
// Customize a model container in the app
import SwiftUI
import SwiftData
@main
struct TripsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Trip.self,
inMemory: true,
isAutosaveEnabled: true,
isUndoEnabled: true)
}
}
In addition, we have the freedom to create model containers more freely:
// Add a model container to the app
import SwiftUI
import SwiftData
@main
struct TripsApp: App {
var container: ModelContainer = {
do {
let configuration = ModelConfiguration(schema: Schema([Trip.self]), url: fileURL)
return try ModelContainer(for: Trip.self, configurations: configuration)
}
catch { ... }
}()
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
Good news! Starting with iOS 18, we can further create a custom data warehouse Data Stores that has more robust, freer, and persistent back-end support. This is the first time we can create our own datastore in SwiftData:
// Use your own custom data store
import SwiftUI
import SwiftData
@main
struct TripsApp: App {
var container: ModelContainer = {
do {
let configuration = JSONStoreConfiguration(schema: Schema([Trip.self]), url: jsonFileURL)
return try ModelContainer(for: Trip.self, configurations: configuration)
}
catch { ... }
}()
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
To learn more about Data Stores customization, you can take a closer look at the WWDC24 video lessons in the figure below:
5. Xcode preview Traits
As you know, SwiftData and SwiftUI can be described as “two-sword combination,” and Xcode preview (Preview) has excellent support for SwiftUI interface debugging.
In iOS 18, we can let the SwiftData data model continue to play well in the preview of Xcode. This can be done through the previewed Trait mechanism:
// Make preview data using traits
struct SampleData: PreviewModifier {
static func makeSharedContext() throws -> ModelContainer {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: Trip.self, configurations: config)
Trip.makeSampleTrips(in: container)
return container
}
func body(content: Content, context: ModelContainer) -> some View {
content.modelContainer(context)
}
}
extension PreviewTrait where T == Preview.ViewTraits {
@MainActor static var sampleData: Self = .modifier(SampleData())
}
In the code above, we let SampleData follow the PreviewModifier protocol and know how to implement two of them.
In this way, using the SwiftData data source provided by SampleData in the Xcode preview becomes the general simplicity of “sachet”:
// Use sample data in a preview
import SwiftUI
import SwiftData
struct ContentView: View {
@Query
var trips: [Trip]
var body: some View {
...
}
}
#Preview(traits: .sampleData) {
ContentView()
}
6. Customize Extra Data Queries
In the debugging of the Xcode preview, for some views, they do not contain the query properties themselves, but they need to be passed in from the outside.
Starting with iOS 18, SwiftData also provides more “ginkgo-like” support. Now we can insert the desired query state directly into the closure attached to the preview #Preview, which is achieved through the @Previewable macro:
// Create a preview query using @Previewable
import SwiftUI
import SwiftData
#Preview(traits: .sampleData) {
@Previewable @Query var trips: [Trip]
BucketListItemView(trip: trips.first)
}
With new @Previewable macros, creating a query dataset for a particular view has become unprecedented.
7. #Expression Expression Macros
Starting with iOS 17, Apple has introduced a new #Predicate macro that gives us the freedom to build assertions about data queries (Predicate):
// Create a Predicate to find a Trip based on Search Text
let predicate = #Predicate<Trip> {
searchText.isEmpty ? true : $0.name.localizedStandardContains(searchText)
}
In addition to simple conditions, we can compose complex multi-condition queries in #Predicate closures:
// Create a Compound Predicate to find a Trip based on Search Text
let predicate = #Predicate<Trip> {
searchText.isEmpty ? true :
0.name.localizedStandardContains(searchText) ||0.destination.localizedStandardContains(searchText)
}
Today, our freedom in iOS 18+ is further enhanced with the times. Now we can use the new #Expression macros to make the asserted Table Danone force “soar”:
// Build a predicate to find Trips with BucketListItems that are not in the plan
let unplannedItemsExpression = #Expression<[BucketListItem], Int> { items in
items.filter {
!$0.isInPlan
}.count
}
let today = Date.now
let tripsWithUnplannedItems = #Predicate<Trip>{ trip
// The current date falls within the trip
(trip.startDate ..< trip.endDate).contains(today) &&
// The trip has at least one BucketListItem
// where 'isInPlan' is false
unplannedItemsExpression.evaluate(trip.bucketList) > 0
}
As the code above shows, we created the unplannedItemsExpression expression with the new #Expression macro and applied it to the tripsWithUnplannedItems assertion to create the most complex and expressive composite query condition ever created.
8. #Index Macros
As you all know, adding an index to a specific field in a database query operation can greatly increase a table query speed.
Starting with iOS 18, the database indexing mechanism was finally explicitly added to SwiftData, which was implemented through #Index macros.
For further information about Apple database-related indexing mechanisms, interested partners can move on to the following links:
- SwiftUI Background Refresh of Multiple Section Causes Resolution of global index in collection view and Actual Mismatch
- Explore how CoreData accelerates table lookup using index mechanisms?
It’s a new SwiftData feature that makes us happy!
With the new #Index macros, we can add lookup table indexes to specific attributes that are frequently accessed in data types to increase query efficiency.
As shown in the following code, we can not only add indexes on a single property, but also apply indexes to multiple property combinations.
// Add Index for commonly used KeyPaths or combination of KeyPaths
import SwiftData
@Model
class Trip {
#Unique<Trip>([\.name, \.startDate, \.endDate
#Index<Trip>([\.name], [\.startDate], [\.endDate], [\.name, \.startDate, \.endDate])
var name: String
var destination: String
var startDate: Date
var endDate: Date
var bucketList: [BucketListItem] = [BucketListItem
var livingAccommodation: LivingAccommodation
}
Adding indexes to managed types is not only beneficial to the speed of table lookup, but also to the filtering and sorting of massive data. 💯
Summary
In this blog post, we introduced the “reinstall upgrade” of the SwiftData framework in iOS 18. I think that #Expression and #Index macro actually help our little partner more, what do you think? Welcome to the discussion.
Thank you for watching and meeting again! 😎
This document is transferred from https://blog.csdn.net/mydo/article/details/139698782,If there is any infringement,Please contact to delete。