Efficiently managing and accessing related data with Swift State Graph's normalization module.
The StateGraphNormalization module provides tools for organizing your data in a normalized structure, making it easier to handle complex relationships between entities. This approach eliminates data duplication, ensures consistency, and integrates seamlessly with Swift State Graph's reactive system.
Data normalization involves storing entities in separate collections while maintaining relationships through references. This pattern is particularly useful for applications with complex data relationships like social networks, content management systems, or e-commerce platforms.
- Single Source of Truth: Entities are stored once, preventing duplication and inconsistencies
- Efficient Updates: Changes to an entity are automatically reflected in all computed properties
- Relationship Management: Easily handle one-to-many and many-to-many relationships
- Performance: Optimized for fast lookups and updates through ID-based access
- Reactivity: Combined with State Graph's dependency tracking for automatic UI updates
EntityStore is a generic container for managing collections of entities with unique identifiers:
import StateGraphNormalization
// Creating entity stores for different types
let userStore = EntityStore<User>()
let postStore = EntityStore<Post>()
let commentStore = EntityStore<Comment>()
// Adding entities
userStore.add(user)
postStore.add(post)
// Retrieving entities
let user = userStore.get(by: userId)
let allUsers = userStore.getAll()
// Filtering entities
let activeUsers = userStore.filter { !$0.isDeleted }
// Updating entities
userStore.update(updatedUser)
// or modify in place
userStore.modify(userId) { user in
user.name = "New Name"
}
// Checking conditions
let hasUser = userStore.contains(userId)
let count = userStore.count
// Removing entities
userStore.delete(userId)The EntityStore provides a comprehensive API for entity management:
// Bulk operations
userStore.add([user1, user2, user3])
// Subscript access
userStore[userId] = updatedUser
let user = userStore[userId]
// Collection properties
if userStore.isEmpty {
print("No users found")
}
print("Total users: \(userStore.count)")Entities stored in an EntityStore must conform to the TypedIdentifiable protocol, which provides type-safe identifiers:
import TypedIdentifier
final class User: TypedIdentifiable {
typealias TypedIdentifierRawValue = String
let typedID: TypedID
@GraphStored
var name: String
@GraphStored
var email: String
@GraphStored
var isActive: Bool
init(id: String, name: String, email: String) {
self.typedID = .init(id)
self.name = name
self.email = email
self.isActive = true
}
}
// Usage with type-safe IDs
let userId: User.TypedID = .init("user123")
let user = userStore.get(by: userId)- Type Safety: IDs are strongly typed, preventing mix-ups between different entity types
- Compiler Checking: Catch ID-related errors at compile time
- Clear Intent: Code explicitly shows which type of entity an ID refers to
NormalizedStore acts as a central repository for managing multiple entity types:
final class NormalizedStore {
@GraphStored
var users: EntityStore<User> = .init()
@GraphStored
var posts: EntityStore<Post> = .init()
@GraphStored
var comments: EntityStore<Comment> = .init()
@GraphStored
var tags: EntityStore<Tag> = .init()
}
// Create a single store instance for your app
let store = NormalizedStore()final class Author: TypedIdentifiable {
typealias TypedIdentifierRawValue = String
let typedID: TypedID
@GraphStored var name: String
// Computed property for posts by this author
@GraphComputed var posts: [Post]
init(id: String, name: String, store: NormalizedStore) {
self.typedID = .init(id)
self.name = name
// Define the relationship
self.$posts = .init { _ in
store.posts.filter { $0.authorId == self.id }
}
}
}
final class Post: TypedIdentifiable {
typealias TypedIdentifierRawValue = String
let typedID: TypedID
@GraphStored var title: String
@GraphStored var content: String
// Reference to author
let authorId: Author.TypedID
init(id: String, title: String, content: String, authorId: Author.TypedID) {
self.typedID = .init(id)
self.title = title
self.content = content
self.authorId = authorId
}
}final class Post: TypedIdentifiable {
typealias TypedIdentifierRawValue = String
let typedID: TypedID
@GraphStored var title: String
@GraphStored var tagIds: Set<Tag.TypedID>
// Computed property for related tags
@GraphComputed var tags: [Tag]
init(id: String, title: String, store: NormalizedStore) {
self.typedID = .init(id)
self.title = title
self.tagIds = []
self.$tags = .init { [$tagIds] _ in
$tagIds.wrappedValue.compactMap { tagId in
store.tags.get(by: tagId)
}
}
}
}
final class Tag: TypedIdentifiable {
typealias TypedIdentifierRawValue = String
let typedID: TypedID
@GraphStored var name: String
// Computed property for posts with this tag
@GraphComputed var posts: [Post]
init(id: String, name: String, store: NormalizedStore) {
self.typedID = .init(id)
self.name = name
self.$posts = .init { _ in
store.posts.filter { post in
post.tagIds.contains(self.id)
}
}
}
}Here's a comprehensive example of a social media application using normalization:
// Entity definitions
final class User: TypedIdentifiable {
typealias TypedIdentifierRawValue = String
let typedID: TypedID
@GraphStored var name: String
@GraphStored var email: String
@GraphStored var followerIds: Set<User.TypedID>
@GraphComputed var posts: [Post]
@GraphComputed var followers: [User]
@GraphComputed var followerCount: Int
init(id: String, name: String, email: String, store: NormalizedStore) {
self.typedID = .init(id)
self.name = name
self.email = email
self.followerIds = []
self.$posts = .init { _ in
store.posts.filter { $0.authorId == self.id }
}
self.$followers = .init { [$followerIds] _ in
$followerIds.wrappedValue.compactMap { followerId in
store.users.get(by: followerId)
}
}
self.$followerCount = .init { [$followers] _ in
$followers.wrappedValue.count
}
}
}
final class Post: TypedIdentifiable {
typealias TypedIdentifierRawValue = String
let typedID: TypedID
@GraphStored var title: String
@GraphStored var content: String
@GraphStored var likes: Int
let authorId: User.TypedID
@GraphComputed var author: User?
@GraphComputed var comments: [Comment]
@GraphComputed var commentCount: Int
init(id: String, title: String, content: String, authorId: User.TypedID, store: NormalizedStore) {
self.typedID = .init(id)
self.title = title
self.content = content
self.likes = 0
self.authorId = authorId
self.$author = .init { _ in
store.users.get(by: self.authorId)
}
self.$comments = .init { _ in
store.comments.filter { $0.postId == self.id }
}
self.$commentCount = .init { [$comments] _ in
$comments.wrappedValue.count
}
}
}
final class Comment: TypedIdentifiable {
typealias TypedIdentifierRawValue = String
let typedID: TypedID
@GraphStored var text: String
@GraphStored var likes: Int
let postId: Post.TypedID
let authorId: User.TypedID
@GraphComputed var author: User?
@GraphComputed var post: Post?
init(id: String, text: String, postId: Post.TypedID, authorId: User.TypedID, store: NormalizedStore) {
self.typedID = .init(id)
self.text = text
self.likes = 0
self.postId = postId
self.authorId = authorId
self.$author = .init { _ in
store.users.get(by: self.authorId)
}
self.$post = .init { _ in
store.posts.get(by: self.postId)
}
}
}
// Usage
let store = NormalizedStore()
// Create users
let alice = User(id: "alice", name: "Alice", email: "alice@example.com", store: store)
let bob = User(id: "bob", name: "Bob", email: "bob@example.com", store: store)
store.users.add([alice, bob])
// Create posts
let post = Post(
id: "post1",
title: "Hello World",
content: "My first post!",
authorId: alice.id,
store: store
)
store.posts.add(post)
// Create comments
let comment = Comment(
id: "comment1",
text: "Great post!",
postId: post.id,
authorId: bob.id,
store: store
)
store.comments.add(comment)
// Access relationships
print("Posts by Alice: \(alice.posts.count)")
print("Comments on post: \(post.commentCount)")
print("Comment author: \(comment.author?.name ?? "Unknown")")Create collections that automatically update based on complex criteria:
final class FeedViewModel {
let store: NormalizedStore
let currentUserId: User.TypedID
@GraphComputed var feedPosts: [Post]
@GraphComputed var trendingPosts: [Post]
init(store: NormalizedStore, currentUserId: User.TypedID) {
self.store = store
self.currentUserId = currentUserId
// Posts from followed users
self.$feedPosts = .init { _ in
guard let currentUser = store.users.get(by: currentUserId) else { return [] }
let followedUserIds = currentUser.followerIds
return store.posts.filter { post in
followedUserIds.contains(post.authorId)
}.sorted { $0.likes > $1.likes }
}
// Posts with high engagement
self.$trendingPosts = .init { _ in
store.posts.filter { $0.likes > 10 }
.sorted { $0.likes > $1.likes }
.prefix(20)
.map { $0 }
}
}
}Keep UI state synchronized with normalized data:
final class PostDetailViewModel {
@GraphStored var selectedPostId: Post.TypedID?
@GraphComputed var selectedPost: Post?
@GraphComputed var relatedPosts: [Post]
init(store: NormalizedStore) {
self.$selectedPost = .init { [$selectedPostId] _ in
guard let postId = $selectedPostId.wrappedValue else { return nil }
return store.posts.get(by: postId)
}
self.$relatedPosts = .init { [$selectedPost] _ in
guard let post = $selectedPost.wrappedValue else { return [] }
// Find posts by the same author
return store.posts.filter { otherPost in
otherPost.authorId == post.authorId && otherPost.id != post.id
}
}
}
}Normalized data works seamlessly with SwiftUI:
struct PostListView: View {
let store: NormalizedStore
var body: some View {
List(store.posts.getAll()) { post in
PostRow(post: post)
}
}
}
struct PostRow: View {
let post: Post
var body: some View {
VStack(alignment: .leading) {
Text(post.title)
.font(.headline)
Text("by \(post.author?.name ?? "Unknown")")
.font(.caption)
.foregroundColor(.secondary)
Text("\(post.commentCount) comments • \(post.likes) likes")
.font(.caption)
}
}
}Use indexed lookups when possible:
// ✅ Efficient: Direct lookup
let user = store.users.get(by: userId)
// ❌ Inefficient: Linear search
let user = store.users.getAll().first { $0.id == userId }Group related changes together:
// Add multiple entities at once
store.users.add([user1, user2, user3])
// Modify entities in batch
userIds.forEach { userId in
store.users.modify(userId) { user in
user.isActive = false
}
}Swift State Graph's normalization module provides a powerful foundation for building applications with complex data relationships while maintaining the benefits of reactive programming.