From f0b4fc3d04d67a35ac82b035972f4e8bf5014686 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 10 May 2026 14:29:18 +0700 Subject: [PATCH 1/2] polish(ios): empty state actions and VoiceOver custom actions for swipe-only deletes --- CHANGELOG.md | 3 +++ TableProMobile/TableProMobile/Views/DataBrowserView.swift | 5 +++++ .../TableProMobile/Views/GroupManagementView.swift | 6 ++++++ TableProMobile/TableProMobile/Views/QueryEditorView.swift | 8 ++++++-- .../TableProMobile/Views/TagManagementView.swift | 7 +++++++ 5 files changed, 27 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33915bb5b..287e056d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- iOS: VoiceOver "Delete row" / "Delete group" / "Delete tag" custom actions on rows whose only deletion path was a swipe gesture +- iOS: empty Groups and Tags screens show a Create button so the action is reachable without opening the toolbar +- iOS: "No Results" empty state in Query Editor explains the query returned no rows - iOS: iCloud sync runs every 30 minutes in the background via `BGAppRefreshTask` while the app is closed (gated by the iCloud Sync setting); iOS schedules the actual cadence based on usage and battery - iOS: Cmd+F focuses the search field in Tables and Data Browser (iPad keyboard canonical) - iOS: search text in Tables and Data Browser persists across process kill via `@SceneStorage` (per-window on iPad) diff --git a/TableProMobile/TableProMobile/Views/DataBrowserView.swift b/TableProMobile/TableProMobile/Views/DataBrowserView.swift index 3b0ce4d46..688a199d3 100644 --- a/TableProMobile/TableProMobile/Views/DataBrowserView.swift +++ b/TableProMobile/TableProMobile/Views/DataBrowserView.swift @@ -267,6 +267,11 @@ struct DataBrowserView: View { .tint(.red) } } + .accessibilityAction(named: Text("Delete row")) { + guard !isView, viewModel.hasPrimaryKeys, !connection.safeModeLevel.blocksWrites else { return } + deleteTarget = viewModel.primaryKeyValues(for: row) + showDeleteConfirmation = true + } } @ViewBuilder diff --git a/TableProMobile/TableProMobile/Views/GroupManagementView.swift b/TableProMobile/TableProMobile/Views/GroupManagementView.swift index 8c744fa2b..c058713f1 100644 --- a/TableProMobile/TableProMobile/Views/GroupManagementView.swift +++ b/TableProMobile/TableProMobile/Views/GroupManagementView.swift @@ -46,6 +46,9 @@ struct GroupManagementView: View { } .tint(.red) } + .accessibilityAction(named: Text("Delete group")) { + groupToDelete = group + } } .onMove { source, destination in var sorted = appState.groups.sorted(by: { $0.sortOrder < $1.sortOrder }) @@ -62,6 +65,9 @@ struct GroupManagementView: View { Label("No Groups", systemImage: "folder") } description: { Text("Create a group to organize your connections.") + } actions: { + Button("Create Group") { showingAddGroup = true } + .buttonStyle(.borderedProminent) } } } diff --git a/TableProMobile/TableProMobile/Views/QueryEditorView.swift b/TableProMobile/TableProMobile/Views/QueryEditorView.swift index 703967c7c..384ce4672 100644 --- a/TableProMobile/TableProMobile/Views/QueryEditorView.swift +++ b/TableProMobile/TableProMobile/Views/QueryEditorView.swift @@ -207,8 +207,12 @@ struct QueryEditorView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) } else if viewModel.legacyRows.isEmpty { - ContentUnavailableView("No Results", systemImage: "tray") - .frame(maxWidth: .infinity, maxHeight: .infinity) + ContentUnavailableView( + "No Results", + systemImage: "tray", + description: Text("The query returned no rows.") + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) } else { resultList } diff --git a/TableProMobile/TableProMobile/Views/TagManagementView.swift b/TableProMobile/TableProMobile/Views/TagManagementView.swift index 062cec239..7fd3ce153 100644 --- a/TableProMobile/TableProMobile/Views/TagManagementView.swift +++ b/TableProMobile/TableProMobile/Views/TagManagementView.swift @@ -47,6 +47,10 @@ struct TagManagementView: View { } } } + .accessibilityAction(named: Text("Delete tag")) { + guard !tag.isPreset else { return } + appState.deleteTag(tag.id) + } } } .overlay { @@ -55,6 +59,9 @@ struct TagManagementView: View { Label("No Tags", systemImage: "tag") } description: { Text("Create a tag to organize your connections.") + } actions: { + Button("Create Tag") { showingAddTag = true } + .buttonStyle(.borderedProminent) } } } From 87260c2e8fdeefa0c052efd86a67c132b063ea3e Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 10 May 2026 14:30:04 +0700 Subject: [PATCH 2/2] fix(structure): inherit row emphasis propagation and deleted-row tint from DataGridRowView --- TablePro/Views/Results/DataGridRowView.swift | 2 +- .../Views/Structure/StructureRowViewWithMenu.swift | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/TablePro/Views/Results/DataGridRowView.swift b/TablePro/Views/Results/DataGridRowView.swift index efc893d32..6634fb78d 100644 --- a/TablePro/Views/Results/DataGridRowView.swift +++ b/TablePro/Views/Results/DataGridRowView.swift @@ -7,7 +7,7 @@ import AppKit import Combine @MainActor -final class DataGridRowView: NSTableRowView { +class DataGridRowView: NSTableRowView { weak var coordinator: TableViewCoordinator? var rowIndex: Int = 0 diff --git a/TablePro/Views/Structure/StructureRowViewWithMenu.swift b/TablePro/Views/Structure/StructureRowViewWithMenu.swift index 545c624d0..35c73ce31 100644 --- a/TablePro/Views/Structure/StructureRowViewWithMenu.swift +++ b/TablePro/Views/Structure/StructureRowViewWithMenu.swift @@ -8,13 +8,17 @@ import AppKit -/// Row view providing a context menu tailored to the Structure tab -final class StructureRowViewWithMenu: NSTableRowView { - weak var coordinator: TableViewCoordinator? - var rowIndex: Int = 0 +/// Row view providing a context menu tailored to the Structure tab. +/// Inherits selection/emphasis propagation, deleted-row tint, and layer-backing +/// from `DataGridRowView` so structure cells repaint on selection like data-tab cells. +final class StructureRowViewWithMenu: DataGridRowView { var structureTab: StructureTab = .columns var isStructureEditable: Bool = true - var isRowDeleted: Bool = false + var isRowDeleted: Bool = false { + didSet { + applyVisualState(RowVisualState(isDeleted: isRowDeleted, isInserted: false, modifiedColumns: [])) + } + } var referencedTableName: String? var onCopyName: ((Set) -> Void)?