Nook uses observable models for runtime state and SwiftData entities for persistence. Models are designed to be reactive, type-safe, and compatible with SwiftUI’s observation system.
Location: Nook/Models/Tab/Tab.swift:18Pattern: Hybrid @Observable + ObservableObject (dual observation for legacy compatibility)The Tab model represents a browser tab with its state, webview, and metadata.
@MainActorpublic class Tab: NSObject, Identifiable, ObservableObject, WKDownloadDelegate { public let id: UUID var url: URL var name: String var favicon: SwiftUI.Image var spaceId: UUID? var profileId: UUID? var index: Int // Pin state var isPinned: Bool = false // Global pinned (essentials) var isSpacePinned: Bool = false // Space-level pinned var folderId: UUID? // Folder membership // Ephemeral state var isEphemeral: Bool { resolveProfile()?.isEphemeral ?? false } // Loading state var loadingState: LoadingState = .idle // Published properties for Combine compatibility @Published var canGoBack: Bool = false @Published var canGoForward: Bool = false @Published var hasPlayingAudio: Bool = false @Published var isAudioMuted: Bool = false @Published var hasPlayingVideo: Bool = false}
Key features:
Lazy webview: Tab.webView computed property creates webview on first access
Favicon caching: Global LRU cache with persistent disk storage (Tab.swift:56-70)
Location: Nook/Models/Space/Space.swift:16Pattern: @Observable (Swift Observation)Spaces organize tabs into themed workspaces with custom gradients.
@MainActor@Observablepublic class Space: NSObject, Identifiable { public let id: UUID var name: String var icon: String var color: NSColor var gradient: SpaceGradient var activeTabId: UUID? var profileId: UUID? var isEphemeral: Bool = false}
Related models:
SpaceGradient (SpaceGradient.swift) — Gradient configuration with nodes and colors
GradientNode (GradientNode.swift) — Individual gradient stop with position/color
Location: Nook/Models/Profile/Profile.swift:16Pattern: @Observable (Swift Observation)Profiles provide isolated browsing contexts with separate data stores.
@MainActor@Observablefinal class Profile: NSObject, Identifiable { let id: UUID var name: String var icon: String let dataStore: WKWebsiteDataStore // Isolated per profile var isEphemeral: Bool = false // Incognito profile var isDefault: Bool { name.lowercased() == "default" } // Cached stats private(set) var cachedCookieCount: Int = 0 private(set) var cachedRecordCount: Int = 0}
Key features:
Data isolation: Each profile gets a unique WKWebsiteDataStore(forIdentifier: profileId) (Profile.swift:82-98)
Ephemeral profiles: Use WKWebsiteDataStore.nonPersistent() for incognito mode
Stats caching: Cookie/record counts cached for performance
Location: Nook/Models/BrowserWindowState.swift:15Pattern: @Observable (Swift Observation)Represents per-window UI state, allowing multiple windows with independent selections.
@MainActor@Observableclass BrowserWindowState { let id: UUID // Active selections var currentTabId: UUID? var currentSpaceId: UUID? var currentProfileId: UUID? var activeTabForSpace: [UUID: UUID] = [:] // UI state var sidebarWidth: CGFloat = 250 var isSidebarVisible: Bool = true var isCommandPaletteVisible: Bool = false var urlBarFrame: CGRect = .zero // Toast state var profileSwitchToast: BrowserManager.ProfileSwitchToast? var isShowingProfileSwitchToast: Bool = false // Incognito state var isIncognito: Bool = false var ephemeralProfile: Profile? var ephemeralSpaces: [Space] = [] // References weak var tabManager: TabManager? weak var commandPalette: CommandPalette? var window: NSWindow?}
Usage: Each ContentView creates a BrowserWindowState and registers it with WindowRegistry.
Location: Nook/Models/Tab/TabsModel.swift:12Persists tab state across app launches.
@Modelfinal class TabEntity { @Attribute(.unique) var id: UUID var urlString: String var name: String var isPinned: Bool var isSpacePinned: Bool var index: Int var spaceId: UUID? var profileId: UUID? // For global pinned tabs var folderId: UUID? // For folder membership // Navigation state tracking var currentURLString: String? var canGoBack: Bool = false var canGoForward: Bool = false}
Location: Nook/Models/Space/SpaceModels.swiftPersists space configuration.
@Modelfinal class SpaceEntity { @Attribute(.unique) var id: UUID var name: String var icon: String var index: Int var gradientData: Data? // Encoded SpaceGradient var activeTabId: UUID? var profileId: UUID?}
Location: Nook/Models/Tab/TabsModel.swift:61Persists tab folders within spaces.
@Modelfinal class FolderEntity { @Attribute(.unique) var id: UUID var name: String var icon: String var color: String var spaceId: UUID var isOpen: Bool var index: Int}
@Modelfinal class ExtensionEntity { @Attribute(.unique) var id: String // Extension unique identifier var displayName: String var version: String var isEnabled: Bool var installDate: Date var manifestData: Data}
Location: Nook/Managers/TabManager/TabManager.swift:33-82Lightweight, serializable representations for atomic persistence.
struct SnapshotTab: Codable { let id: UUID let urlString: String let name: String let index: Int let spaceId: UUID? let isPinned: Bool let isSpacePinned: Bool let profileId: UUID? let folderId: UUID? let currentURLString: String? let canGoBack: Bool let canGoForward: Bool}struct SnapshotSpace: Codable { ... }struct SnapshotFolder: Codable { ... }struct SnapshotState: Codable { ... }struct Snapshot: Codable { let spaces: [SnapshotSpace] let tabs: [SnapshotTab] let folders: [SnapshotFolder] let state: SnapshotState}
Purpose: Decouple persistence from observable models to avoid actor isolation issues.
Use @Observable for new modelsSwift Observation (@Observable) is the modern approach with better performance and cleaner syntax. Only use @Published for legacy compatibility.
Keep models MainActor-confinedAll models that interact with SwiftUI must be @MainActor to prevent data races.
Use snapshots for async persistenceDon’t pass observable models directly to actors. Convert to Codable snapshots first to avoid actor isolation errors.
Validate entity → model conversionsAlways handle missing/corrupt data when restoring from SwiftData. Use default values or skip invalid entities.