SwiftData in WWDC 2024: The Revolution Continues, Stability Still Awaits

Published on

Get weekly handpicked updates on Swift and SwiftUI!

Since its debut last year, SwiftData has become a focal framework that has garnered significant attention from developers. With the arrival of WWDC 2024, there is widespread anticipation for breakthroughs in functionality, performance, and stability in SwiftData. This article will review the performance of the latest version of SwiftData and share the complex emotions I experienced during my first encounter with the new version: shock, joy, disheartened, and perplexed.

The Revolution in Data Management Frameworks Continues

At WWDC 2023, SwiftData debuted with the significant responsibility of being Apple’s key data management framework for the next decade or more. Since its first release, this “successor” to Core Data has left a profound and favorable impression on everyone due to its deep understanding of modern programming principles and its potential capabilities.

There are two main reasons for calling it the successor to Core Data: First, it is set to take over the role of Core Data in the Apple ecosystem for a long time; second, in its initial version, SwiftData was highly associated with Core Data, allowing developers to find many corresponding components in SwiftData. Based on this association, I developed the SwiftDataKit library, enabling developers direct access to the underlying Core Data implementations of SwiftData components.

In the article New Frameworks, New Mindset: Unveiling the Observation and SwiftData Frameworks, I praised the design philosophy of SwiftData, especially its innovations in data modeling and concurrent operations, which undoubtedly revolutionize Core Data.

I had expected that after the transformative first release, the SwiftData team would focus on improving stability and adding more features. However, the updates at WWDC 2024 completely overturned my expectations—SwiftData had rewritten its underlying data storage logic, breaking away from its close coupling with Core Data and undergoing extensive abstraction and division. This move truly shocked me.

At WWDC 2024, SwiftData evolved into a modern data management framework that fully utilizes new features of the Swift language, with efficient data modeling capabilities, secure concurrent operation mechanisms, straightforward predicate articulations, and compatibility with various underlying data storage types. Its integration with SwiftUI has also become more seamless.

Starting with this version, SwiftData can no longer be considered as being built on Core Data. We can only say that the default storage format supported by SwiftData remains consistent with Core Data. Moreover, from the next version onward, the underlying processing and operations between the database and the data may no longer rely on Core Data’s code.

In the current version, using -com.apple.CoreData.SQLDebug still allows observation of Core Data’s operational info.

Although SwiftUI has launched six versions over five years, it has never experienced such profound changes to its underlying structure. In contrast, SwiftData daring to implement such extensive changes in just its second year is commendable for its courage, but these drastic changes also raise concerns about the new version’s stability.

The adjustments to the data storage logic, although necessary in the long term, are regrettable not to have been implemented in the first version.

Due to the adjustments in storage logic, SwiftDataKit is no longer applicable to the updated SwiftData.

New Features Brought by WWDC 2024

Although at first glance it appears that not many new features were introduced in this update, the method of their implementation and the significant potential impact have brought about joyous surprises.

Custom Data Storage

In this update, SwiftData has undergone a significant transformation, now allowing developers to define the underlying storage format through custom implementations that comply with the DataStore and DataStoreConfiguration protocols. This feature enables developers to use various data storage methods at the backend, such as files, different types of databases, or cloud databases, while maintaining the same frontend code.

When using ModelConfiguration (or DataStoreConfiguration) to build a ModelContainer, the system defaults to using DefaultStore (DataStore), which supports the Core Data storage format.

This revolutionary change, although it does not majorly affect the everyday use of most SwiftData users—since the changes in the code are almost imperceptible—could potentially cause stability issues for some time.

For more details about this feature, watch Create a custom data store with SwiftData. I will explore this functionality more deeply in future articles.

Swift History

The initial version of SwiftData did not include a feature similar to Core Data’s persistent history tracking, and the absence of willSave and didSave notifications limited developers’ ability to automatically monitor data changes outside of the app. Fortunately, this shortcoming has been addressed in the latest update.

The DataStore protocol now includes an access interface for data change history, and modelContext also provides a corresponding API. Although these new APIs and the history data format are more modern and align with the Swift language style, their operational logic and handling methods remain very similar to Core Data’s persistent history tracking.

To learn more about this feature, watch Track model changes with SwiftData history.

Batch Deletion of Data

The DataStore protocol now includes functionality for batch operations, primarily supporting batch deletion. SwiftData’s DefaultStore has implemented this API, enabling developers to leverage the underlying batch operation logic when invoking delete<T>(model: T.Type, where predicate: Predicate<T>?, includeSubclasses: Bool) and deleteAllData (most likely).

New Annotations

  • #Unique Macro: This macro is used to define uniqueness constraints. Compared to the unique option in the Attribute macro, the #Unique macro adds support for composite constraints across multiple properties. Since the default storage implementation of SwiftData still relies on SQLite’s constraint mechanisms, data models using this macro will not be compatible with CloudKit’s synchronization rules.
Swift
@Model
final class Person {
    // Declare any unique constraints as part of the model definition.
    #Unique<Person>([\.id], [\.givenName, \.familyName])

    var id: UUID
    var givenName: String
    var familyName: String


    init(id: UUID, givenName: String, familyName: String) {
        self.id = id
        self.givenName = givenName
        self.familyName = familyName
    }
}
  • #Index Macro: This macro enables the creation of indexes for single or multiple properties, thereby enhancing retrieval efficiency. For attributes frequently used in searches and sorting, indexing can significantly accelerate query speeds. However, the use of indexes also occupies more storage space and may affect write performance. Therefore, creating indexes for attributes that are often used as query conditions or sorting criteria, and involve large datasets, can yield tangible benefits.
Swift
@Model 
class Trip {
    #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
}
  • preserveValueOnDeletion Option: By adding the preserveValueOnDeletion option in the Attribute macro, the content of the attribute will still be preserved in the deletion records of the data change history even if the data is deleted. This allows developers to perform subsequent processing based on the specific content in the historical records.
Swift
@Model 
class Trip {
    @Attribute(.preserveValueOnDeletion)
    var startDate: Date

    @Attribute(.preserveValueOnDeletion)
    var endDate: Date
}

Enhanced Preview Environment Setup

With the updates at WWDC 2024, previewing views containing SwiftData data has become more convenient and secure.

  • PreviewTrait: The latest update to SwiftUI introduced the PreviewModifier protocol, which allows developers to easily customize PreviewTraits to build a safe and complete environment context for previews. The code below demonstrates how to implement a PreviewModifier, which creates a modelContainer for the preview view, generates demo data, and injects a context instance.
Swift
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())
}
  • @Previewable Macro: This macro greatly simplifies the process for developers to construct preview wrapper views. The example code below demonstrates how to automatically create a wrapper view for the preview and retrieve relevant data within it using @Query. Both the modelContext and the demo data are provided by the previously defined PreviewTrait.
Swift
struct TripDetail: View {
    let trip: Trip?
    var body: some View {
        ...
    }
}

#Preview(traits: .sampleData) {
    @Previewable @Query var trips: [Trip]
    TripDetail(trip: trips.first)
}

Building Complex Predicates Has Become Easier

At WWDC 2024, the Foundation’s predicate system introduced several new features, including new expression methods, with the most significant improvement being the addition of the #Expression macro, which greatly simplifies the construction of predicate expressions. In previous versions, developers could only experience this convenience when using the #Predicate macro. Now, with the #Expression macro, even the construction of standalone expressions can be seamlessly and naturally executed.

The #Expression macro allows developers to define predicates using multiple independent expressions, not only clarifying the construction of complex predicates but also enhancing the reusability of expressions.

Unlike predicates, which only return boolean values, expressions can return any type. Therefore, when declaring expressions, developers need to specify the input and output types explicitly.

Swift
let unplannedItemsExpression = #Expression<[BucketListItem], Int> { items in
    items.filter {
        !$0.isInPlan
    }.count
}

let today = Date.now
let tripsWithUnplannedItems = #Predicate<Trip>{ trip in
    // 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
}

Although the #Expression macro is a highly valuable tool, but the enhanced expression capabilities do not guarantee that SwiftData’s DefaultStore can correctly translate predicates into corresponding SQL commands. I have not yet conducted extensive testing to determine if the current version resolves issues with accurately converting predicates that involve optional and to-many relationships. If you have any relevant information, please notify me through X or leave a comment in the section below this article.

To learn how to dynamically construct predicates in SwiftData, please read How to Dynamically Construct Complex Predicates for SwiftData.

Have the Shortcomings of the Previous Version Been Addressed?

In the article Before WWDC 2024: The Future Potential and Real Challenges of SwiftData, I listed the key features and main issues missing from the first version of SwiftData. After initial testing, at least the following problems and needs have not been resolved:

  • Network synchronization still only supports private databases.
  • When using @ModalActor to perform data operations in non-main contexts, views still fail to respond correctly to data changes (even performing worse than in the previous version).
  • Performance issues still exist when dealing with multi-relationship data entries.
  • The notification functions for didSave and willSave are still not operational.

Given the poor performance in stability of the first version, I plan to conduct more thorough testing after some time.

Frankly, given the current state of issue resolution, I am quite disheartened.

Is Now a Good Time to Use SwiftData in Projects?

In recent days, I’ve received numerous inquiries from developers about whether it’s advisable to use SwiftData in new projects at this stage. I am somewhat perplexed at the moment.

Functionally, although still lacking in some areas, the updated SwiftData is capable of satisfying the requirements of most application scenarios. However, I currently lack confidence in its stability, especially considering the significant adjustments made to the underlying structure in this update, which makes it difficult to predict the short-term impact on stability.

Therefore, I recommend that developers who have not yet used SwiftData avoid deploying it in actual projects for the next one to two months. Wait until its stability has been further verified before considering its use. Of course, during this period, it is crucial to delve into SwiftData’s documentation and articles and learn about its new design philosophy.

I hope that SwiftData will soon prove its reliability in practical applications.

SwiftData, Another Contribution by Apple to the Swift Community?

I imagine many readers might feel disheartened at this moment—such an excellent framework, why isn’t optimizing stability a priority?

Throughout the writing of this article, one question has persistently haunted me: Why would Apple make such significant adjustments to the framework just one year after its release? What is the purpose of these changes? How will they affect future developments and prospects?

Through an in-depth study of the new APIs, I have found that SwiftData has largely decoupled from the Apple ecosystem. In other words, it is already laying the groundwork to become a cross-platform open-source Swift framework. If SwiftData can further provide a platform-independent default storage implementation, we are likely to see its application in non-Apple ecosystems over the next few years, making it a significant tool for enhancing the cross-platform influence of the Swift language.

This may well be the reason why SwiftData continues to undergo substantial transformations. When it eventually goes open source, it will mark the beginning of another revolutionary era! We look forward to that day arriving soon.

Weekly Swift & SwiftUI insights, delivered every Monday night. Join developers worldwide.
Easy unsubscribe, zero spam guaranteed