Tab lifecycle, persistence, and organization in Nook Browser
The TabManager is responsible for managing the complete lifecycle of tabs in Nook, including creation, organization into spaces and folders, pinning, and atomic persistence using SwiftData.
actor PersistenceActor { struct Snapshot: Codable { let spaces: [SnapshotSpace] let tabs: [SnapshotTab] let folders: [SnapshotFolder] let state: SnapshotState } 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? // For global pinned tabs let folderId: UUID? // For folder membership let currentURLString: String? let canGoBack: Bool let canGoForward: Bool }}
The PersistenceActor maintains a lightweight JSON backup of the most recent snapshot in memory. On catastrophic failure, it can restore from this backup to prevent total data loss.
func setActiveSpace(_ space: Space) { // 1. Remember active tab for outgoing space if let prevSpace = currentSpace, let prevTab = currentTab { prevSpace.activeTabId = prevTab.id } // 2. Trigger gradient transition browserManager?.refreshGradientsForSpace(space, animate: true) // 3. Switch to new space currentSpace = space // 4. Restore last active tab for this space var targetTab: Tab? if let activeId = space.activeTabId { targetTab = findTab(byId: activeId) // Checks regular, space-pinned, global pinned } // Fallback to first tab in space or global pinned if targetTab == nil { targetTab = tabsBySpace[space.id]?.first ?? spacePinnedTabs[space.id]?.first ?? pinnedTabs.first } // 5. Update active tab and notify extensions if targetTab?.id != currentTab?.id { currentTab = targetTab if #available(macOS 15.5, *), let newActive = currentTab { ExtensionManager.shared.notifyTabActivated(newTab: newActive, previous: previousTab) } } persistSnapshot()}
func createNewTab(url: String = "https://www.google.com", in space: Space?) -> Tab { let targetSpace = space ?? currentSpace ?? ensureDefaultSpaceIfNeeded() // Increment existing tab indices to insert at top let existingTabs = tabsBySpace[targetSpace.id] ?? [] for tab in existingTabs { tab.index += 1 } let newTab = Tab( url: validURL, name: "New Tab", favicon: "globe", spaceId: targetSpace.id, index: 0, // New tabs appear at top browserManager: browserManager ) addTab(newTab) setActiveTab(newTab) return newTab}
New tabs are inserted at index 0 (top of the list), and existing tabs are shifted down.Source: Nook/Managers/TabManager/TabManager.swift:1147-1194
func removeTab(_ id: UUID) { // Notify SplitViewManager to prevent zombie state browserManager?.splitManager.handleTabClosure(id) // Find and remove from container var removed: Tab? for space in spaces { if var arr = tabsBySpace[space.id], let i = arr.firstIndex(where: { $0.id == id }) { removed = arr.remove(at: i) setTabs(arr, for: space.id) break } } // Track for undo (20 second window) if let tab = removed { trackRecentlyClosedTab(tab, spaceId: removedSpaceId) } // Force unload from compositor browserManager?.compositorManager.unloadTab(removed) browserManager?.webViewCoordinator?.removeAllWebViews(for: removed) // Notify extension system if #available(macOS 15.5, *) { ExtensionManager.shared.notifyTabClosed(removed) } persistSnapshot()}
Always call removeTab(_:) instead of directly manipulating tab arrays. This ensures proper cleanup of WebViews, compositor state, split view state, and extension notifications.
The handleDragOperation(_:) method supports moving tabs between all container types:
enum DragContainer { case essentials // Global pinned case spacePinned(UUID) // Space-pinned area case spaceRegular(UUID) // Regular tabs in space case folder(UUID) // Folder within space case none}struct DragOperation { let tab: Tab let fromContainer: DragContainer let toContainer: DragContainer let toIndex: Int}
Example: Moving from regular tab to essentials:
case (.spaceRegular(_), .essentials): guard browserManager?.currentProfile?.id != nil else { return } removeFromCurrentContainer(tab) tab.spaceId = nil withCurrentProfilePinnedArray { arr in let safeIndex = max(0, min(operation.toIndex, arr.count)) arr.insert(tab, at: safeIndex) } persistSnapshot()
For multi-window support, TabManager coordinates with WebViewCoordinator:
// When removing a tab, clean up all window-specific WebViewsfunc removeTab(_ id: UUID) { // ... browserManager?.webViewCoordinator?.removeAllWebViews(for: tab)}
TabManager validates window states after structural changes:
func removeSpace(_ id: UUID) { // ... remove space and tabs ... // Validate all windows still have valid tab references browserManager?.validateWindowStates()}