diff --git a/TablePro/AppDelegate.swift b/TablePro/AppDelegate.swift index 25a9b0c38..544a9874f 100644 --- a/TablePro/AppDelegate.swift +++ b/TablePro/AppDelegate.swift @@ -15,6 +15,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { private var hasRunPostLaunchActivation = false private var pluginsRejectedCancellable: AnyCancellable? + private var commandCancellables: Set = [] // MARK: - URL & File Open @@ -83,18 +84,18 @@ class AppDelegate: NSObject, NSApplicationDelegate { .sink { [weak self] rejected in self?.handlePluginsRejected(rejected) } - NotificationCenter.default.addObserver( - self, selector: #selector(handleFocusConnectionForm), - name: .focusConnectionFormWindowRequested, object: nil - ) - NotificationCenter.default.addObserver( - self, selector: #selector(handleOpenSampleDatabase(_:)), - name: .openSampleDatabaseRequested, object: nil - ) - NotificationCenter.default.addObserver( - self, selector: #selector(handleResetSampleDatabase(_:)), - name: .resetSampleDatabaseRequested, object: nil - ) + AppCommands.shared.focusConnectionFormWindowRequested + .receive(on: RunLoop.main) + .sink { [weak self] _ in self?.handleFocusConnectionForm() } + .store(in: &commandCancellables) + AppCommands.shared.openSampleDatabaseRequested + .receive(on: RunLoop.main) + .sink { _ in SampleDatabaseLauncher.open() } + .store(in: &commandCancellables) + AppCommands.shared.resetSampleDatabaseRequested + .receive(on: RunLoop.main) + .sink { _ in SampleDatabaseLauncher.reset() } + .store(in: &commandCancellables) } func applicationDidBecomeActive(_ notification: Notification) { @@ -210,20 +211,12 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } - @objc func handleFocusConnectionForm() { + private func handleFocusConnectionForm() { if let window = NSApp.windows.first(where: { AppLaunchCoordinator.isConnectionFormWindow($0) }) { window.makeKeyAndOrderFront(nil) } } - @objc func handleOpenSampleDatabase(_ notification: Notification) { - SampleDatabaseLauncher.open() - } - - @objc func handleResetSampleDatabase(_ notification: Notification) { - SampleDatabaseLauncher.reset() - } - // MARK: - Dock Menu func applicationDockMenu(_ sender: NSApplication) -> NSMenu? { diff --git a/TablePro/Core/Database/DatabaseManager+Schema.swift b/TablePro/Core/Database/DatabaseManager+Schema.swift index 10b53456b..a38ef703b 100644 --- a/TablePro/Core/Database/DatabaseManager+Schema.swift +++ b/TablePro/Core/Database/DatabaseManager+Schema.swift @@ -5,6 +5,7 @@ // Created by Ngo Quoc Dat on 16/12/25. // +import Combine import Foundation import os import TableProPluginKit @@ -89,8 +90,9 @@ extension DatabaseManager { ) } - // Post notification to refresh UI - NotificationCenter.default.post(name: .refreshData, object: nil) + await MainActor.run { + AppCommands.shared.refreshData.send(nil) + } } catch { if useTransaction { do { diff --git a/TablePro/Core/Events/AppCommands.swift b/TablePro/Core/Events/AppCommands.swift new file mode 100644 index 000000000..1d458f549 --- /dev/null +++ b/TablePro/Core/Events/AppCommands.swift @@ -0,0 +1,42 @@ +// +// AppCommands.swift +// TablePro +// + +import Combine +import Foundation + +@MainActor +final class AppCommands { + static let shared = AppCommands() + + // MARK: - Row Commands + + let deleteSelectedRows = PassthroughSubject() + let addNewRow = PassthroughSubject() + let duplicateRow = PassthroughSubject() + let copySelectedRows = PassthroughSubject() + let pasteRows = PassthroughSubject() + + // MARK: - Refresh + + let refreshData = PassthroughSubject() + + // MARK: - File / Connection Import-Export + + let openSQLFiles = PassthroughSubject<[URL], Never>() + let exportConnections = PassthroughSubject() + let importConnections = PassthroughSubject() + let importConnectionsFromApp = PassthroughSubject() + let exportQueryResults = PassthroughSubject() + let saveAsFavoriteRequested = PassthroughSubject() + + // MARK: - Window / Sheet Commands + + let focusConnectionFormWindowRequested = PassthroughSubject() + let openSampleDatabaseRequested = PassthroughSubject() + let resetSampleDatabaseRequested = PassthroughSubject() + let presentDatabaseTypeChooser = PassthroughSubject() + + private init() {} +} diff --git a/TablePro/Core/KeyboardHandling/ResponderChainActions.swift b/TablePro/Core/KeyboardHandling/ResponderChainActions.swift index 342eb2373..1941ef3d9 100644 --- a/TablePro/Core/KeyboardHandling/ResponderChainActions.swift +++ b/TablePro/Core/KeyboardHandling/ResponderChainActions.swift @@ -20,10 +20,10 @@ // - Clean method calls, no global event bus // - Commands are automatically nil (disabled) when no connection is active // -// 3. **NotificationCenter** (Multi-listener broadcasts only): -// - `.refreshData` (Sidebar + Coordinator + StructureView) -// - Non-menu notifications from AppKit views (DataGrid, SidebarView context menus) -// - Legitimate broadcasts where multiple views respond +// 3. **AppCommands** (Multi-listener broadcasts only): +// - `refreshData` (Sidebar + Coordinator + StructureView) +// - Non-menu commands from AppKit views (DataGrid, SidebarView context menus) +// - Typed Combine publishers for broadcasts where multiple views respond // // ## Example Flow // diff --git a/TablePro/Core/Services/Infrastructure/AppNotifications.swift b/TablePro/Core/Services/Infrastructure/AppNotifications.swift deleted file mode 100644 index 52c8b6ee9..000000000 --- a/TablePro/Core/Services/Infrastructure/AppNotifications.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// AppNotifications.swift -// TablePro -// -// Centralized notification names used across the app. -// Domain-specific collections remain in TableProApp.swift -// and SettingsNotifications.swift. -// - -import Foundation - -extension Notification.Name { - // MARK: - Connections - - static let exportConnections = Notification.Name("exportConnections") - static let importConnections = Notification.Name("importConnections") - static let importConnectionsFromApp = Notification.Name("importConnectionsFromApp") - static let focusConnectionFormWindowRequested = Notification.Name("focusConnectionFormWindowRequested") - static let openSampleDatabaseRequested = Notification.Name("openSampleDatabaseRequested") - static let resetSampleDatabaseRequested = Notification.Name("resetSampleDatabaseRequested") - - // MARK: - Export - - static let exportQueryResults = Notification.Name("exportQueryResults") - - // MARK: - SQL Favorites - - static let saveAsFavoriteRequested = Notification.Name("saveAsFavoriteRequested") - - // MARK: - Feedback - - static let showFeedbackWindow = Notification.Name("com.TablePro.showFeedbackWindow") -} diff --git a/TablePro/Core/Services/Infrastructure/MainWindowToolbar.swift b/TablePro/Core/Services/Infrastructure/MainWindowToolbar.swift index 38831a8db..6b4116422 100644 --- a/TablePro/Core/Services/Infrastructure/MainWindowToolbar.swift +++ b/TablePro/Core/Services/Infrastructure/MainWindowToolbar.swift @@ -15,6 +15,7 @@ // import AppKit +import Combine import os import SwiftUI import TableProPluginKit @@ -302,7 +303,7 @@ private struct RefreshToolbarButton: View { var body: some View { let state = coordinator.toolbarState Button { - NotificationCenter.default.post(name: .refreshData, object: nil) + AppCommands.shared.refreshData.send(nil) } label: { Label("Refresh", systemImage: "arrow.clockwise") } diff --git a/TablePro/Core/Services/Infrastructure/WelcomeRouter.swift b/TablePro/Core/Services/Infrastructure/WelcomeRouter.swift index 43ef059f5..4f2b2aa89 100644 --- a/TablePro/Core/Services/Infrastructure/WelcomeRouter.swift +++ b/TablePro/Core/Services/Infrastructure/WelcomeRouter.swift @@ -30,7 +30,7 @@ internal final class WelcomeRouter { private func drainPendingSQLFiles() { let urls = consumePendingSQLFiles() guard !urls.isEmpty else { return } - NotificationCenter.default.post(name: .openSQLFiles, object: urls) + AppCommands.shared.openSQLFiles.send(urls) } internal func routeImport(_ exportable: ExportableConnection) { diff --git a/TablePro/TableProApp.swift b/TablePro/TableProApp.swift index c7951ffec..fc43be46c 100644 --- a/TablePro/TableProApp.swift +++ b/TablePro/TableProApp.swift @@ -6,6 +6,7 @@ // import CodeEditTextView +import Combine import Observation import os import Sparkle @@ -201,11 +202,11 @@ struct AppMenuCommands: Commands { .optionalKeyboardShortcut(shortcut(for: .manageConnections)) Button(String(localized: "Open Sample Database")) { - NotificationCenter.default.post(name: .openSampleDatabaseRequested, object: nil) + AppCommands.shared.openSampleDatabaseRequested.send(()) } Button(String(localized: "Reset Sample Database...")) { - NotificationCenter.default.post(name: .resetSampleDatabaseRequested, object: nil) + AppCommands.shared.resetSampleDatabaseRequested.send(()) } } @@ -265,15 +266,15 @@ struct AppMenuCommands: Commands { Divider() Button(String(localized: "Export Connections...")) { - NotificationCenter.default.post(name: .exportConnections, object: nil) + AppCommands.shared.exportConnections.send(()) } Button(String(localized: "Import Connections...")) { - NotificationCenter.default.post(name: .importConnections, object: nil) + AppCommands.shared.importConnections.send(()) } Button(String(localized: "Import from Other App...")) { - NotificationCenter.default.post(name: .importConnectionsFromApp, object: nil) + AppCommands.shared.importConnectionsFromApp.send(()) } Divider() @@ -350,7 +351,7 @@ struct AppMenuCommands: Commands { .disabled(!(actions?.isQueryExecuting ?? false)) Button("Refresh") { - NotificationCenter.default.post(name: .refreshData, object: nil) + AppCommands.shared.refreshData.send(nil) } .optionalKeyboardShortcut(shortcut(for: .refresh)) .disabled(!(actions?.isConnected ?? false)) @@ -681,23 +682,6 @@ struct TableProApp: App { } } -// MARK: - Notification Names - -extension Notification.Name { - // Multi-listener broadcasts (Sidebar + Coordinator + StructureView) - static let refreshData = Notification.Name("refreshData") - - // Data operations (still posted by DataGrid / context menus) - static let deleteSelectedRows = Notification.Name("deleteSelectedRows") - static let addNewRow = Notification.Name("addNewRow") - static let duplicateRow = Notification.Name("duplicateRow") - static let copySelectedRows = Notification.Name("copySelectedRows") - static let pasteRows = Notification.Name("pasteRows") - - // File opening notifications - static let openSQLFiles = Notification.Name("openSQLFiles") -} - // MARK: - Check for Updates /// Menu bar button that triggers Sparkle update check diff --git a/TablePro/ViewModels/WelcomeViewModel.swift b/TablePro/ViewModels/WelcomeViewModel.swift index 56d69be0d..c0952b7e5 100644 --- a/TablePro/ViewModels/WelcomeViewModel.swift +++ b/TablePro/ViewModels/WelcomeViewModel.swift @@ -81,9 +81,9 @@ final class WelcomeViewModel { @ObservationIgnored private var connectionUpdatedCancellable: AnyCancellable? @ObservationIgnored private var linkedFoldersCancellable: AnyCancellable? - @ObservationIgnored private var exportObserver: NSObjectProtocol? - @ObservationIgnored private var importObserver: NSObjectProtocol? - @ObservationIgnored private var importFromAppObserver: NSObjectProtocol? + @ObservationIgnored private var exportConnectionsCancellable: AnyCancellable? + @ObservationIgnored private var importConnectionsCancellable: AnyCancellable? + @ObservationIgnored private var importFromAppCancellable: AnyCancellable? @ObservationIgnored private var welcomeRouterTask: Task? @ObservationIgnored private var searchDebounceTask: Task? private static let searchDebounceNanoseconds: UInt64 = 150_000_000 @@ -166,30 +166,24 @@ final class WelcomeViewModel { self?.loadConnections() } - exportObserver = NotificationCenter.default.addObserver( - forName: .exportConnections, object: nil, queue: .main - ) { [weak self] _ in - Task { @MainActor [weak self] in + exportConnectionsCancellable = AppCommands.shared.exportConnections + .receive(on: RunLoop.main) + .sink { [weak self] _ in guard let self, !self.connections.isEmpty else { return } self.activeSheet = .exportConnections(self.connections) } - } - importObserver = NotificationCenter.default.addObserver( - forName: .importConnections, object: nil, queue: .main - ) { [weak self] _ in - Task { @MainActor [weak self] in + importConnectionsCancellable = AppCommands.shared.importConnections + .receive(on: RunLoop.main) + .sink { [weak self] _ in self?.importConnectionsFromFile() } - } - importFromAppObserver = NotificationCenter.default.addObserver( - forName: .importConnectionsFromApp, object: nil, queue: .main - ) { [weak self] _ in - Task { @MainActor [weak self] in + importFromAppCancellable = AppCommands.shared.importConnectionsFromApp + .receive(on: RunLoop.main) + .sink { [weak self] _ in self?.activeSheet = .importFromApp } - } linkedFoldersCancellable = AppEvents.shared.linkedFoldersDidUpdate .receive(on: RunLoop.main) @@ -264,11 +258,6 @@ final class WelcomeViewModel { deinit { welcomeRouterTask?.cancel() searchDebounceTask?.cancel() - [exportObserver, importObserver, importFromAppObserver].forEach { - if let observer = $0 { - NotificationCenter.default.removeObserver(observer) - } - } } // MARK: - Data Loading @@ -580,7 +569,7 @@ final class WelcomeViewModel { } func focusConnectionFormWindow() { - NotificationCenter.default.post(name: .focusConnectionFormWindowRequested, object: nil) + AppCommands.shared.focusConnectionFormWindowRequested.send(()) } // MARK: - Private Helpers diff --git a/TablePro/Views/Connection/WelcomeWindowView.swift b/TablePro/Views/Connection/WelcomeWindowView.swift index 0967ede23..0f2fee95a 100644 --- a/TablePro/Views/Connection/WelcomeWindowView.swift +++ b/TablePro/Views/Connection/WelcomeWindowView.swift @@ -118,11 +118,7 @@ struct WelcomeWindowView: View { chooserState: $welcomeChooserState, urlImportPresented: $urlImportPresented )) - .onReceive(NotificationCenter.default.publisher(for: .presentDatabaseTypeChooser)) { note in - guard - let payload = note.userInfo?[DatabaseTypeChooserPayload.userInfoKey] - as? DatabaseTypeChooserPayload - else { return } + .onReceive(AppCommands.shared.presentDatabaseTypeChooser) { payload in welcomeChooserState = WelcomeChooserState( initialType: payload.initialType, onSelected: { type in diff --git a/TablePro/Views/Import/ImportDialog.swift b/TablePro/Views/Import/ImportDialog.swift index 91a1d3e31..3205f0d6c 100644 --- a/TablePro/Views/Import/ImportDialog.swift +++ b/TablePro/Views/Import/ImportDialog.swift @@ -6,6 +6,7 @@ // import AppKit +import Combine import os import SwiftUI import TableProPluginKit @@ -102,7 +103,7 @@ struct ImportDialog: View { } .sheet(isPresented: $showSuccessDialog, onDismiss: { isPresented = false - NotificationCenter.default.post(name: .refreshData, object: connection.id) + AppCommands.shared.refreshData.send(connection.id) }) { ImportSuccessView( result: importResult diff --git a/TablePro/Views/Infrastructure/WindowOpenerBridge.swift b/TablePro/Views/Infrastructure/WindowOpenerBridge.swift index 4625c34a5..687cce781 100644 --- a/TablePro/Views/Infrastructure/WindowOpenerBridge.swift +++ b/TablePro/Views/Infrastructure/WindowOpenerBridge.swift @@ -3,6 +3,7 @@ // TablePro // +import Combine import SwiftUI internal struct WindowOpenerBridge: View { @@ -26,19 +27,13 @@ internal struct WindowOpenerBridge: View { initialType: initialType, onSelected: onSelected ) - NotificationCenter.default.post( - name: .presentDatabaseTypeChooser, - object: nil, - userInfo: [DatabaseTypeChooserPayload.userInfoKey: payload] - ) + AppCommands.shared.presentDatabaseTypeChooser.send(payload) } ) } } internal final class DatabaseTypeChooserPayload { - static let userInfoKey = "DatabaseTypeChooserPayload" - let initialType: DatabaseType? let onSelected: (DatabaseType) -> Void @@ -47,7 +42,3 @@ internal final class DatabaseTypeChooserPayload { self.onSelected = onSelected } } - -internal extension Notification.Name { - static let presentDatabaseTypeChooser = Notification.Name("com.TablePro.presentDatabaseTypeChooser") -} diff --git a/TablePro/Views/Main/Child/DataTabGridDelegate.swift b/TablePro/Views/Main/Child/DataTabGridDelegate.swift index b61c609ab..fc7908bab 100644 --- a/TablePro/Views/Main/Child/DataTabGridDelegate.swift +++ b/TablePro/Views/Main/Child/DataTabGridDelegate.swift @@ -7,6 +7,7 @@ // import AppKit +import Combine @MainActor final class DataTabGridDelegate: DataGridViewDelegate { @@ -65,7 +66,7 @@ final class DataTabGridDelegate: DataGridViewDelegate { } func dataGridExportResults() { - NotificationCenter.default.post(name: .exportQueryResults, object: nil) + AppCommands.shared.exportQueryResults.send(()) } func dataGridUndo() {} diff --git a/TablePro/Views/Main/Child/MainEditorContentView.swift b/TablePro/Views/Main/Child/MainEditorContentView.swift index ce36a2c4d..7773320dd 100644 --- a/TablePro/Views/Main/Child/MainEditorContentView.swift +++ b/TablePro/Views/Main/Child/MainEditorContentView.swift @@ -130,8 +130,7 @@ struct MainEditorContentView: View { } ) } - .onReceive(NotificationCenter.default.publisher(for: .saveAsFavoriteRequested)) { notification in - guard let query = notification.userInfo?["query"] as? String else { return } + .onReceive(AppCommands.shared.saveAsFavoriteRequested) { query in favoriteDialogQuery = FavoriteDialogQuery(query: query) } .onChange(of: tabManager.tabStructureVersion) { _, _ in diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+Favorites.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+Favorites.swift index 61ed79d81..85efe58a4 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+Favorites.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+Favorites.swift @@ -4,6 +4,7 @@ // import AppKit +import Combine import Foundation extension MainContentCoordinator { @@ -31,11 +32,7 @@ extension MainContentCoordinator { tab.tabType == .query else { return } let query = tab.content.query.trimmingCharacters(in: .whitespacesAndNewlines) guard !query.isEmpty else { return } - NotificationCenter.default.post( - name: .saveAsFavoriteRequested, - object: nil, - userInfo: ["query": query] - ) + AppCommands.shared.saveAsFavoriteRequested.send(query) } func openLinkedFavorite(_ favorite: LinkedSQLFavorite) { diff --git a/TablePro/Views/Main/MainContentCommandActions.swift b/TablePro/Views/Main/MainContentCommandActions.swift index 7b2811037..d5c1795af 100644 --- a/TablePro/Views/Main/MainContentCommandActions.swift +++ b/TablePro/Views/Main/MainContentCommandActions.swift @@ -111,6 +111,20 @@ final class MainContentCommandActions { } } + /// Subscribes to an `AppCommands` publisher and only runs the handler when this instance's window is key. + private func observeKeyWindowOnly( + _ publisher: PassthroughSubject, + handler: @escaping @MainActor (Payload) -> Void + ) { + publisher + .receive(on: RunLoop.main) + .sink { [weak self] payload in + guard self?.isKeyWindow() == true else { return } + handler(payload) + } + .store(in: &eventCancellables) + } + // MARK: - Save Action private func setupSaveAction() { @@ -147,27 +161,23 @@ final class MainContentCommandActions { /// context menus, QueryEditorView, ConnectionStatusView). These bridge AppKit/non-menu /// notification posts to the same command action methods used by @FocusedValue callers. private func setupNonMenuNotificationObservers() { - observeKeyWindowOnly(.addNewRow) { [weak self] _ in self?.addNewRow() } + observeKeyWindowOnly(AppCommands.shared.addNewRow) { [weak self] _ in self?.addNewRow() } - observeKeyWindowOnly(.deleteSelectedRows) { [weak self] notification in - let directIndices = notification.userInfo?["rowIndices"] as? Set - self?.deleteSelectedRows(rowIndices: directIndices) + observeKeyWindowOnly(AppCommands.shared.deleteSelectedRows) { [weak self] _ in + self?.deleteSelectedRows() } - observeKeyWindowOnly(.duplicateRow) { [weak self] _ in self?.duplicateRow() } + observeKeyWindowOnly(AppCommands.shared.duplicateRow) { [weak self] _ in self?.duplicateRow() } - observeKeyWindowOnly(.exportQueryResults) { [weak self] _ in self?.exportQueryResults() } + observeKeyWindowOnly(AppCommands.shared.exportQueryResults) { [weak self] _ in self?.exportQueryResults() } - // Note: .copySelectedRows and .pasteRows observers call the data-grid - // path directly (not the public methods) to avoid an infinite loop — - // the public methods re-post these notifications for structure view. - observeKeyWindowOnly(.copySelectedRows) { [weak self] _ in + observeKeyWindowOnly(AppCommands.shared.copySelectedRows) { [weak self] _ in guard let self else { return } let indices = self.selectionState.indices self.coordinator?.copySelectedRowsToClipboard(indices: indices) } - observeKeyWindowOnly(.pasteRows) { [weak self] _ in + observeKeyWindowOnly(AppCommands.shared.pasteRows) { [weak self] _ in self?.coordinator?.pasteRows() } } @@ -634,7 +644,7 @@ final class MainContentCommandActions { func openSQLFile() { Task { guard let urls = await SQLFileService.showOpenPanel() else { return } - NotificationCenter.default.post(name: .openSQLFiles, object: urls) + AppCommands.shared.openSQLFiles.send(urls) } } @@ -800,16 +810,19 @@ final class MainContentCommandActions { // MARK: Data Broadcasts private func setupDataBroadcastObservers() { - observe(.refreshData) { [weak self] notification in - guard let self else { return } - if let target = notification.object as? UUID, target != self.connection.id { - return - } - if notification.object == nil && !self.isKeyWindow() { - return + AppCommands.shared.refreshData + .receive(on: RunLoop.main) + .sink { [weak self] target in + guard let self else { return } + if let target, target != self.connection.id { + return + } + if target == nil && !self.isKeyWindow() { + return + } + self.handleRefreshData() } - self.handleRefreshData() - } + .store(in: &eventCancellables) } private func handleRefreshData() { @@ -872,13 +885,12 @@ final class MainContentCommandActions { // MARK: File Open Broadcasts private func setupFileOpenObservers() { - observeKeyWindowOnly(.openSQLFiles) { [weak self] notification in - self?.handleOpenSQLFiles(notification) + observeKeyWindowOnly(AppCommands.shared.openSQLFiles) { [weak self] urls in + self?.handleOpenSQLFiles(urls) } } - private func handleOpenSQLFiles(_ notification: Notification) { - guard let urls = notification.object as? [URL] else { return } + private func handleOpenSQLFiles(_ urls: [URL]) { Task { for url in urls { try? await TabRouter.shared.route(.openSQLFile(url)) diff --git a/TablePro/Views/Structure/CreateTableView.swift b/TablePro/Views/Structure/CreateTableView.swift index 062f86bca..576f52f4f 100644 --- a/TablePro/Views/Structure/CreateTableView.swift +++ b/TablePro/Views/Structure/CreateTableView.swift @@ -7,6 +7,7 @@ // import AppKit +import Combine import os import SwiftUI import TableProPluginKit @@ -348,7 +349,7 @@ struct CreateTableView: View { wasSuccessful: true ) - NotificationCenter.default.post(name: .refreshData, object: nil) + AppCommands.shared.refreshData.send(nil) if let coordinator { coordinator.openTableTab(tableName) diff --git a/TablePro/Views/Structure/TableStructureView+DataLoading.swift b/TablePro/Views/Structure/TableStructureView+DataLoading.swift index 60c4f6164..fb3274e39 100644 --- a/TablePro/Views/Structure/TableStructureView+DataLoading.swift +++ b/TablePro/Views/Structure/TableStructureView+DataLoading.swift @@ -127,7 +127,7 @@ extension TableStructureView { loadSchemaForEditing() } - func onRefreshData(_ notification: Notification) { + func onRefreshData() { // Ignore refresh notifications while we're in the middle of our own save/reload guard !isReloadingAfterSave else { Self.logger.debug("Ignoring refresh notification - currently reloading after save") diff --git a/TablePro/Views/Structure/TableStructureView.swift b/TablePro/Views/Structure/TableStructureView.swift index 0e9f77579..325a0bebc 100644 --- a/TablePro/Views/Structure/TableStructureView.swift +++ b/TablePro/Views/Structure/TableStructureView.swift @@ -118,7 +118,7 @@ struct TableStructureView: View { coordinator?.toolbarState.hasStructureChanges = newValue updateGridDelegate() } - .onReceive(NotificationCenter.default.publisher(for: .refreshData), perform: onRefreshData) + .onReceive(AppCommands.shared.refreshData) { _ in onRefreshData() } } // MARK: - Toolbar @@ -247,7 +247,7 @@ struct TableStructureView: View { loadSchemaForEditing() isReloadingAfterSave = false columnLayoutPersister.clear(for: tableName, connectionId: connection.id) - NotificationCenter.default.post(name: .refreshData, object: nil) + AppCommands.shared.refreshData.send(nil) } catch { AlertHelper.showErrorSheet( title: String(localized: "Column Reorder Failed"),