whalebeings.com

Unlocking SwiftData: Using It Beyond SwiftUI Framework

Written on

Introduction to SwiftData

SwiftData is the latest persistence framework that offers a modern approach to data management. It includes convenient features like property wrappers, such as @Environment.modelContext and @Query, which simplify how developers access and manipulate persistent data directly from SwiftUI views. While this is particularly appealing to younger developers accustomed to MV architecture, those of us who still favor MVVM and prioritize unit testing often find ourselves at a crossroads.

Apple's insistence on promoting SwiftUI, even in contexts where only 21% of their binaries utilize it, raises questions about the architectural direction they advocate for app development. The navigation APIs have introduced challenges by encouraging developers to merge navigation and persistence logic directly into UI code. This can lead to complex and unmanageable code structures—an issue that becomes especially problematic in larger projects.

Fortunately, utilizing SwiftData outside of SwiftUI views is straightforward.

Exploring SwiftData Beyond SwiftUI

For those interested, my open-source repository, NoSwiftDataNoUI, is available for reference. This project features a simple application that manages a list of stored users. In the absence of existing user data, the app can generate 10,000 random users.

The Model

Our @Model object is uncomplicated, focusing solely on user attributes without intricate relationships:

@Model

final class User {

@Attribute(.unique) let id: UUID

let firstName: String

let surname: String

let age: Int

}

Setting Up the Database

Creating a basic database service only takes a few lines of code:

final class UserDatabase {

let container: ModelContainer

init() throws {

container = try ModelContainer(for: User.self)

}

}

The ModelContainer manages the storage, defaulting to a SQLite file, but it can also be configured to use an in-memory store for testing purposes:

init(useInMemoryStore: Bool = false) throws {

let configuration = ModelConfiguration(for: User.self, isStoredInMemoryOnly: useInMemoryStore)

container = try ModelContainer(for: User.self, configurations: configuration)

}

To use this service within our view models, we can inject it as a dependency. In this simplistic example, I initialized it directly:

@Observable

final class ContentViewModel {

var users: [User] = []

private let database: UserDatabase

init() {

self.database = try! UserDatabase()

}

}

Now that we've established SwiftData's utility outside of SwiftUI, let's delve into CRUD operations.

The first video provides an insightful tutorial on how to persist data in SwiftUI using SwiftData, perfect for developers looking to streamline their data management.

CRUD Operations

Let's implement standard CRUD operations for our UserDatabase.

Create

The create method is straightforward and demonstrates essential concepts:

func create(_ user: T) throws {

let context = ModelContext(container)

context.insert(user)

try context.save()

}

Creating the ModelContext occurs within the function since it's not inherently thread-safe. Storing a context as a property could lead to performance overhead.

Why Use a Context?

The ModelContext functions similarly to ManagedObjectContext in Core Data, tracking changes made to data objects in memory. These changes are committed to the underlying data store through context.save().

For example, if you want to create multiple users at once:

func create(_ users: [T]) throws {

let context = ModelContext(container)

for user in users {

context.insert(user)

}

try context.save()

}

Inserting 10,000 items in one go can significantly enhance performance by minimizing direct disk I/O operations.

Generating Users

To generate a large number of users, I implemented a convenience initializer in the User model that randomizes its properties:

private func generateUsers() {

let users = (0..<10_000).compactMap { _ in try? User() }

try? database.create(users)

}

Update

SwiftData simplifies updates since its insert method functions as an upsert—if the ID exists, it updates the corresponding model object; otherwise, it creates a new one.

Read

To retrieve data, we can invoke context.fetch() using a FetchDescriptor, which allows us to specify what we want to fetch and how to sort the results:

func read(predicate: Predicate?, sortDescriptors: SortDescriptor...) throws -> [User] {

let context = ModelContext(container)

let fetchDescriptor = FetchDescriptor(

predicate: predicate,

sortBy: sortDescriptors

)

return try context.fetch(fetchDescriptor)

}

We can refine our data retrieval further by using predicates and sort descriptors.

Delete

Deleting records is similarly straightforward:

func delete(_ user: User) throws {

let context = ModelContext(container)

let idToDelete = user.persistentModelID

try context.delete(model: User.self, where: #Predicate { user in

user.persistentModelID == idToDelete

})

try context.save()

}

This ensures that we maintain thread safety by utilizing the @Sendable property persistentModelID.

Refactoring for Efficiency

To optimize our code, I introduced protocols, extensions, and generics—elements that are not usually present in a pure SwiftUI application.

Database Protocol

Our overarching protocol does not require any specific SwiftData entities, making it flexible across various implementations:

protocol Database {

associatedtype T

func create(_ item: T) throws

func create(_ items: [T]) throws

func read(predicate: Predicate?, sortDescriptors: SortDescriptor...) throws -> [T]

func update(_ item: T) throws

func delete(_ item: T) throws

}

SwiftDatabase Protocol

Next, we define a SwiftDatabase protocol that is specific to SwiftData:

protocol SwiftDatabase: Database {

associatedtype T = PersistentModel

var container: ModelContainer { get }

}

Creating a Database

With this structure in place, creating our UserDatabase becomes concise:

final class UserDB: SwiftDatabase {

typealias T = User

let container: ModelContainer

init() throws {

container = try ModelContainer(for: User.self)

}

}

Conclusion

This article was inspired by discussions about the limitations of SwiftData APIs, specifically their focus on SwiftUI. While Apple may not make it obvious, using SwiftData outside of SwiftUI is entirely possible, and I hope this guide clarifies how to implement it effectively.

The second video discusses the process of converting a Core Data implementation to SwiftData, providing insights into the transition.

Share the page:

Twitter Facebook Reddit LinkIn

-----------------------

Recent Post:

Accomplish More in Less Time: 8 Effective Strategies

Discover 8 powerful habits to boost productivity and manage time efficiently while balancing work and family life.

Self-Improvement: Embracing Your Own Beauty Journey

Discover the truth about beauty and self-acceptance, and learn how to embrace your unique qualities while pursuing personal growth.

The Significance of Kindness in Our Lives

Understanding the profound impact of kindness amidst negativity and how it can transform our reality.