--- name: Swift Model Design description: Model design patterns including single source of truth and computed properties globs: ["**/*.swift"] --- # Model Design Patterns **Computed properties should be the single source of truth for derived data.** ## Single Source of Truth Principle Never store data that can be computed from other stored data. This prevents sync bugs and simplifies maintenance. ### Name Fields Pattern When a model has multiple name components, use a computed property for the display name: ```swift @Model final class Person { var prefix: String = "" // "Dr.", "Mr.", etc. var firstName: String = "" var middleName: String = "" var lastName: String = "" var suffix: String = "" // "Jr.", "III", etc. var nickname: String = "" // GOOD - Computed from individual fields var fullName: String { var parts: [String] = [] if !prefix.isEmpty { parts.append(prefix) } if !firstName.isEmpty { parts.append(firstName) } if !middleName.isEmpty { parts.append(middleName) } if !lastName.isEmpty { parts.append(lastName) } if !suffix.isEmpty { parts.append(suffix) } return parts.joined(separator: " ") } // For display with nickname var displayName: String { if !nickname.isEmpty { return nickname } if !firstName.isEmpty { return firstName } return fullName } // Plain format for export (no special formatting) var vCardName: String { [firstName, middleName, lastName] .filter { !$0.isEmpty } .joined(separator: " ") } // BAD - Stored displayName that can get out of sync // var storedDisplayName: String // Never add this } ``` ### Benefits of Computed Properties - **Always up to date**: Changes to individual fields are immediately reflected - **No sync bugs**: No risk of stored value diverging from component fields - **Simpler code**: No need to update derived values when editing source fields - **Less storage**: No duplicate data in database ## Derived State Patterns ### Counts and Aggregates ```swift @Model final class Project { var name: String = "" @Relationship(deleteRule: .cascade) var tasks: [Task]? = [] // GOOD - Computed counts var taskCount: Int { tasks?.count ?? 0 } var completedTaskCount: Int { tasks?.filter(\.isCompleted).count ?? 0 } var progress: Double { guard taskCount > 0 else { return 0 } return Double(completedTaskCount) / Double(taskCount) } var isComplete: Bool { taskCount > 0 && completedTaskCount == taskCount } // BAD - Stored counts that need manual updates // var storedTaskCount: Int = 0 // var storedCompletedCount: Int = 0 } ``` ### Status and State ```swift @Model final class Order { var createdAt: Date = Date() var paidAt: Date? var shippedAt: Date? var deliveredAt: Date? var cancelledAt: Date? // GOOD - Computed status var status: OrderStatus { if cancelledAt != nil { return .cancelled } if deliveredAt != nil { return .delivered } if shippedAt != nil { return .shipped } if paidAt != nil { return .paid } return .pending } var isActive: Bool { cancelledAt == nil && deliveredAt == nil } var canCancel: Bool { shippedAt == nil && cancelledAt == nil } } enum OrderStatus: String, Codable { case pending, paid, shipped, delivered, cancelled } ``` ### Validation State ```swift @Model final class UserProfile { var email: String = "" var phone: String = "" var firstName: String = "" var lastName: String = "" // GOOD - Computed validation var isEmailValid: Bool { email.contains("@") && email.contains(".") } var isPhoneValid: Bool { let digits = phone.filter(\.isNumber) return digits.count >= 10 } var isComplete: Bool { !firstName.isEmpty && !lastName.isEmpty && isEmailValid } var validationErrors: [String] { var errors: [String] = [] if firstName.isEmpty { errors.append("First name is required") } if lastName.isEmpty { errors.append("Last name is required") } if !isEmailValid { errors.append("Valid email is required") } return errors } } ``` ## When to Store vs Compute ### Store When: - Data comes from an external source (API, user input) - Computation is expensive and value is accessed frequently - Historical accuracy matters (price at time of purchase) - You need to query/filter by the value in database ### Compute When: - Value is derived from other stored properties - Value can change when source properties change - Keeping values in sync would be error-prone - Computation is fast (string concatenation, simple math) ## Caching Expensive Computations For expensive computed values accessed frequently: ```swift @Observable @MainActor final class AnalyticsStore { private var items: [Item] = [] // Cache invalidation tracking private var itemsVersion = 0 private var cachedStatsVersion = -1 private var cachedStats: Statistics? var statistics: Statistics { // Return cached if valid if cachedStatsVersion == itemsVersion, let cached = cachedStats { return cached } // Compute and cache let stats = computeStatistics() cachedStats = stats cachedStatsVersion = itemsVersion return stats } func updateItems(_ newItems: [Item]) { items = newItems itemsVersion += 1 // Invalidate cache } private func computeStatistics() -> Statistics { // Expensive computation } } ``` ## Identifiable and Hashable Implement `Identifiable` for use with SwiftUI lists: ```swift @Model final class Item { var id: UUID = UUID() // Or use @Attribute(.unique) if not using CloudKit var name: String = "" } // SwiftData models are automatically Identifiable if they have an 'id' property ``` For value types used in Sets or as Dictionary keys: ```swift struct Tag: Hashable, Codable { let id: UUID let name: String // Hashable based on id only func hash(into hasher: inout Hasher) { hasher.combine(id) } static func == (lhs: Tag, rhs: Tag) -> Bool { lhs.id == rhs.id } } ```