The new SwiftData in iOS 18 is reinstalled and upgraded, and one of the features is guaranteed to make you unable to put it down.

Insert image description here

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”?

Insert image description here

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.

Insert image description here

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. Insert image description here

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.

Insert image description here

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):

Insert image description here

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.

Insert image description here

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:

Insert image description here

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.

Insert image description here

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.

Insert image description here

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)
} 

Insert image description here

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.

Insert image description here


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.

Insert image description here

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。