Copilot, Claude, Cursor, and others all read from ~/.agents/. The npx skills CLI handles fan-out to tool-specific directories.
6.4 KiB
6.4 KiB
| name | description | globs | |
|---|---|---|---|
| Swift Model Design | Model design patterns including single source of truth and computed properties |
|
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:
@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
@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
@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
@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:
@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:
@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:
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
}
}