Chapter 3: Data Modeling & Relationships

The Expense Tracker Schema

  • Category: Represents a bucket for expenses (e.g., "Groceries", "Entertainment", "Rent").
  • Transaction: Represents a single financial record, consisting of an amount, a date, and optionally some notes.

Design Decisions: Data Types

  1. Identifiers (UUID vs URIRepresentation): Core Data provides an internal identifier for every object called NSManagedObjectID. However, this ID can change (for instance, a temporary ID becomes a permanent ID after the context is saved). Furthermore, if you ever migrate to a cloud backend or sync across devices, Core Data's internal object IDs will not match across devices. Therefore, we always define our own id attribute of type UUID.
  2. Colors (String vs Custom Value Transformer): We need to store a color for each category. It is tempting to store a UIColor or SwiftUI Color using a Transformable attribute. Do not do this. Storing UI-specific types in the database couples your data layer to a specific UI framework. If you ever port your app to macOS or want to serialize the database to JSON, UIColor is useless. Instead, we store the color as a Hex String (e.g., "#FF5733") and let the UI layer parse it.
erDiagram CATEGORY ||--o{ TRANSACTION : "contains" CATEGORY { UUID id String name String colorHex String iconName } TRANSACTION { UUID id Double amount Date date String notes }
import Foundation
import CoreData

@objc(Transaction)
public class Transaction: NSManagedObject {}

@objc(Category)
public class Category: NSManagedObject {}

extension Transaction {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Transaction> {
        return NSFetchRequest<Transaction>(entityName: "Transaction")
    }

    @NSManaged public var id: UUID?
    @NSManaged public var amount: Double
    @NSManaged public var date: Date?
    @NSManaged public var notes: String?
    @NSManaged public var category: Category?
}

Creating the .xcdatamodeld File

Defining the Entities

  1. Click the Add Entity button at the bottom of the editor.
  2. Rename the new entity to Category.
  3. Add another entity and rename it to Transaction.

[!NOTE] Entity names should always be singular (e.g., Category, not Categories), following standard object-oriented naming conventions. You are defining the blueprint for one object.

Setting Up Attributes

  • id (UUID): A unique identifier for the category.
  • name (String): The display name (e.g., "Food").
  • colorHex (String): A hex code to represent the category's color.
  • iconName (String): An SF Symbols icon name.
  • id (UUID): A unique identifier.
  • amount (Double): The monetary value.
  • date (Date): When the transaction occurred.
  • notes (String): Optional text for extra details.

Optionality and Defaults

  • Optional vs Non-Optional: By default, Xcode makes all attributes optional. This is a defensive mechanism, but in domain modeling, optionality should strictly reflect your business logic. For our domain, id, name, amount, and date should be strictly non-optional. Uncheck the "Optional" checkbox for these. notes can remain optional.
  • Default Values: You can provide default values for non-optional attributes. However, be cautious: setting a default date to "now" in the model editor means the default is the date the model was compiled, not the date the object is created. We will handle initialization programmatically to avoid this trap.

[!CAUTION] If you uncheck "Optional" but fail to provide a value when creating the object in code, your app will crash when calling context.save(). Core Data enforces these validation rules strictly.

Relationships: Tying It All Together

Creating the One-to-Many Relationship

  1. Select the Transaction entity.
  2. In the Relationships section, click the + button.
  3. Name the relationship category.
  4. Set the Destination to Category.
  5. In the Relationships section, click the + button.
  6. Name the relationship transactions.
  7. Set the Destination to Transaction.
  8. In the Data Model Inspector, change the Type from "To One" to "To Many".

The Critical Importance of Inverse Relationships

import Foundation
import CoreData

func assignCategory(myTransaction: Transaction, groceriesCategory: Category) {
    myTransaction.category = groceriesCategory
}
  1. On the Category entity, select the transactions relationship.
  2. In the Data Model Inspector, set the Inverse to category.
  3. Go back to the Transaction entity, select the category relationship, and verify its Inverse is now automatically set to transactions.

Configuring Delete Rules and Performance

  • Nullify (Default): The destination's relationship pointer is set to nil. If we delete "Groceries", its transactions become orphaned (their category property becomes nil).
  • Cascade: Deleting the source deletes all destination objects. If we delete "Groceries", all transactions under "Groceries" are also deleted.
  • Deny: Prevents deletion of the source if any destination objects exist. You can't delete "Groceries" if it has transactions.
  • No Action: Does nothing. The destination object still thinks it points to the deleted object. Never use this unless you are managing the graph manually, as it guarantees a crash if accessed.

[!WARNING] Performance Tip: Cascade deletion requires Core Data to load (fault) every destination object into memory to fire their lifecycle methods (like prepareForDeletion) and delete them one by one. If a Category has 100,000 transactions, deleting the category will cause a massive memory spike and freeze the main thread. In extreme scenarios, you must bypass Cascade and use a NSBatchDeleteRequest to delete the transactions directly in SQLite. For standard iOS apps, however, Cascade is perfectly fine.

flowchart LR A[Category] -->|"transactions (To-Many, Cascade)"| B[Transaction] B -->|"category (To-One, Nullify)"| A style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#bbf,stroke:#333,stroke-width:2px

Code Generation: The "UIKit Way"

  1. Class Definition (Default): Xcode magically generates the class and properties behind the scenes. You don't see the code in your project navigator, and you can't easily add custom properties or domain logic to the class.
  2. Category/Extension: Xcode generates an extension with the properties, but expects you to write the main class definition.
  3. Manual/None: Xcode generates nothing automatically on build. You manually trigger the code generation, giving you 100% control over the output.
  4. Select the Category entity. In the Data Model Inspector, change Codegen to Manual/None.
  5. Repeat this for the Transaction entity.

Generating the NSManagedObject Subclasses

  • Category+CoreDataClass.swift (The class definition)
  • Category+CoreDataProperties.swift (The properties extension containing the @NSManaged attributes)
import Foundation
import CoreData

@objc(Transaction)
public class Transaction: NSManagedObject {}

@objc(Category)
public class Category: NSManagedObject {}

extension Transaction {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Transaction> {
        return NSFetchRequest<Transaction>(entityName: "Transaction")
    }

    // Safely removed optionals for guaranteed properties
    @NSManaged public var id: UUID
    @NSManaged public var amount: Double
    @NSManaged public var date: Date
    @NSManaged public var notes: String?
    @NSManaged public var category: Category
}
import Foundation
import CoreData

@objc(Transaction)
public class Transaction: NSManagedObject {}

@objc(Category)
public class Category: NSManagedObject {}

extension Category {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Category> {
        return NSFetchRequest<Category>(entityName: "Category")
    }

    @NSManaged public var id: UUID
    @NSManaged public var name: String
    @NSManaged public var colorHex: String
    @NSManaged public var iconName: String
    @NSManaged public var transactions: NSSet? // Core Data uses NSSet for To-Many relationships
}

Adding Domain Logic to NSManagedObjects

import Foundation
import CoreData

@objc(Transaction)
public class Transaction: NSManagedObject {
    @NSManaged public var date: Date
}

@objc(Category)
public class Category: NSManagedObject {
    @NSManaged public var transactions: NSSet?
    
    /// A sorted array of transactions for easy consumption.
    /// Safely casts the Objective-C NSSet to a Swift Set, then sorts it.
    var sortedTransactions: [Transaction] {
        let set = transactions as? Set<Transaction> ?? []
        return set.sorted { $0.date > $1.date }
    }
    
    /// Helper method triggered precisely when the object is inserted into the context.
    public override func awakeFromInsert() {
        super.awakeFromInsert()
        
        // Guarantee that 'id' is populated immediately upon creation.
        // We use setPrimitiveValue to avoid triggering KVO notifications during initialization.
        setPrimitiveValue(UUID(), forKey: "id")
    }
}

Verifying Your Model Setup in Xcode

  1. Verify Codegen Settings: For both Category and Expense (or Transaction) entities, select the entity in the project editor, navigate to the Data Model Inspector on the right, and confirm that Codegen is explicitly set to Manual/None. If left on default settings (Class Definition), Xcode will silently generate hidden background duplicate declarations, triggering persistent compilation errors (Invalid redeclaration of class).
  2. Confirm Inverse Relationships: Ensure that neither relationship displays a yellow compiler warning in Xcode. Every relationship must explicitly point back to its counterpart (e.g., Category.transactions inverse is Transaction.category).
  3. Validate Optionality Agreements: Ensure that attributes marked as non-optional in your Swift subclass extensions (such as id or date) possess corresponding initialization guarantees—either through schema-level default values or deterministic runtime assignment inside awakeFromInsert().

Summary & Next Steps

  1. Design Intentionally: We chose portable UUIDs over platform-bound Core Data primary keys for public identification, and standardized on hex formatting strings over UI colors for platform independence.
  2. Configure Relationships: We established bidirectional connections, understanding that Inverse Relationships are mandatory for object graph integrity during runtime graph traversal. We evaluated the critical data retention and performance boundaries between Cascade, Deny, and Nullify deletion rules.
  3. Master Code Generation: We discarded Xcode's automated codegen in favor of the UIKit Way (Manual/None), claiming complete engineering authority over our NSManagedObject subclasses. We eliminated legacy Objective-C optionality artifacts from guaranteed non-nil properties and harnessed awakeFromInsert() for crash-free lifecycle initialization.