From ce37c12a2bbf2403e7b824cdf72831cd9a275e56 Mon Sep 17 00:00:00 2001 From: zunda Date: Sun, 31 May 2026 22:23:54 +0900 Subject: [PATCH 1/2] add SemanticButtons --- .../Views/Components/FKPreviewView.swift | 2 +- .../Views/Components/FilterSheetView.swift | 6 ++- .../Views/Components/GroupFormSheet.swift | 6 ++- .../Views/Components/SemanticButtons.swift | 43 +++++++++++++++++++ .../Views/Components/TagFormSheet.swift | 6 ++- .../TableProMobile/Views/ConnectedView.swift | 2 +- .../Views/ConnectionFormView.swift | 6 +-- .../Views/ConnectionListView.swift | 2 +- .../Views/DataBrowserView.swift | 2 +- .../Views/GroupManagementView.swift | 2 +- .../TableProMobile/Views/InsertRowView.swift | 8 ++-- .../TableProMobile/Views/RowDetailView.swift | 8 ++-- .../TableProMobile/Views/SettingsView.swift | 2 +- .../Views/TagManagementView.swift | 2 +- 14 files changed, 73 insertions(+), 24 deletions(-) create mode 100644 TableProMobile/TableProMobile/Views/Components/SemanticButtons.swift diff --git a/TableProMobile/TableProMobile/Views/Components/FKPreviewView.swift b/TableProMobile/TableProMobile/Views/Components/FKPreviewView.swift index c6fe8aea1..975c4d513 100644 --- a/TableProMobile/TableProMobile/Views/Components/FKPreviewView.swift +++ b/TableProMobile/TableProMobile/Views/Components/FKPreviewView.swift @@ -53,7 +53,7 @@ struct FKPreviewView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .confirmationAction) { - Button("Done") { dismiss() } + CloseButton { dismiss() } } } .task { await loadReferencedRow() } diff --git a/TableProMobile/TableProMobile/Views/Components/FilterSheetView.swift b/TableProMobile/TableProMobile/Views/Components/FilterSheetView.swift index 29bb6cec5..46ba24378 100644 --- a/TableProMobile/TableProMobile/Views/Components/FilterSheetView.swift +++ b/TableProMobile/TableProMobile/Views/Components/FilterSheetView.swift @@ -88,14 +88,16 @@ struct FilterSheetView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { dismiss() } + CancelButton { dismiss() } } ToolbarItem(placement: .confirmationAction) { - Button("Apply") { + ConfirmButton { filters = draft logicMode = draftLogicMode onApply() dismiss() + } label: { + Text("Done") } .disabled(!hasValidFilters) } diff --git a/TableProMobile/TableProMobile/Views/Components/GroupFormSheet.swift b/TableProMobile/TableProMobile/Views/Components/GroupFormSheet.swift index 82f65d0ab..c4da7594f 100644 --- a/TableProMobile/TableProMobile/Views/Components/GroupFormSheet.swift +++ b/TableProMobile/TableProMobile/Views/Components/GroupFormSheet.swift @@ -32,15 +32,17 @@ struct GroupFormSheet: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { dismiss() } + CancelButton { dismiss() } } ToolbarItem(placement: .confirmationAction) { - Button("Save") { + ConfirmButton { var group = existingGroup ?? ConnectionGroup() group.name = name group.color = color onSave(group) dismiss() + } label: { + Text("Done") } .disabled(name.trimmingCharacters(in: .whitespaces).isEmpty) } diff --git a/TableProMobile/TableProMobile/Views/Components/SemanticButtons.swift b/TableProMobile/TableProMobile/Views/Components/SemanticButtons.swift new file mode 100644 index 000000000..827b41b74 --- /dev/null +++ b/TableProMobile/TableProMobile/Views/Components/SemanticButtons.swift @@ -0,0 +1,43 @@ +import SwiftUI + +struct CloseButton: View { + let action: () -> Void + + var body: some View { + if #available(iOS 26.0, *) { + Button(role: .close, action: action) + } else { + Button(String(localized: "Done"), action: action) + } + } +} + +struct CancelButton: View { + let action: () -> Void + + var body: some View { + if #available(iOS 26.0, *) { + Button(role: .cancel, action: action) + } else { + Button("Cancel", role: .cancel, action: action) + } + } +} + +struct ConfirmButton: View { + private let action: () -> Void + private let label: Label + + init(action: @escaping () -> Void, @ViewBuilder label: () -> Label) { + self.action = action + self.label = label() + } + + var body: some View { + if #available(iOS 26.0, *) { + Button(role: .confirm, action: action) + } else { + Button(action: action) { label } + } + } +} diff --git a/TableProMobile/TableProMobile/Views/Components/TagFormSheet.swift b/TableProMobile/TableProMobile/Views/Components/TagFormSheet.swift index 183a92b95..a7989bb6c 100644 --- a/TableProMobile/TableProMobile/Views/Components/TagFormSheet.swift +++ b/TableProMobile/TableProMobile/Views/Components/TagFormSheet.swift @@ -32,15 +32,17 @@ struct TagFormSheet: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { dismiss() } + CancelButton { dismiss() } } ToolbarItem(placement: .confirmationAction) { - Button("Save") { + ConfirmButton { var tag = existingTag ?? ConnectionTag() tag.name = name tag.color = color onSave(tag) dismiss() + } label: { + Text("Done") } .disabled(name.trimmingCharacters(in: .whitespaces).isEmpty) } diff --git a/TableProMobile/TableProMobile/Views/ConnectedView.swift b/TableProMobile/TableProMobile/Views/ConnectedView.swift index 51adad9ea..313b9ceec 100644 --- a/TableProMobile/TableProMobile/Views/ConnectedView.swift +++ b/TableProMobile/TableProMobile/Views/ConnectedView.swift @@ -82,7 +82,7 @@ struct ConnectedView: View { Text(String(format: String(localized: "Connecting to %@..."), connection.name.isEmpty ? connection.host : connection.name)) } - Button(String(localized: "Cancel")) { + CancelButton { dismiss() } .buttonStyle(.bordered) diff --git a/TableProMobile/TableProMobile/Views/ConnectionFormView.swift b/TableProMobile/TableProMobile/Views/ConnectionFormView.swift index 94052dc63..b7c2c8780 100644 --- a/TableProMobile/TableProMobile/Views/ConnectionFormView.swift +++ b/TableProMobile/TableProMobile/Views/ConnectionFormView.swift @@ -81,10 +81,10 @@ struct ConnectionFormView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { dismiss() } + CancelButton { dismiss() } } ToolbarItem(placement: .confirmationAction) { - Button("Save", action: handleSave) + ConfirmButton(action: handleSave) { Text("Done") } .disabled(!viewModel.canSave) } } @@ -104,7 +104,7 @@ struct ConnectionFormView: View { .alert("New Database", isPresented: $showNewDatabaseAlert) { TextField("Database name", text: $viewModel.newDatabaseName) Button("Create") { viewModel.createNewDatabase() } - Button("Cancel", role: .cancel) { viewModel.newDatabaseName = "" } + CancelButton { viewModel.newDatabaseName = "" } } message: { Text("Enter a name for the new SQLite database.") } diff --git a/TableProMobile/TableProMobile/Views/ConnectionListView.swift b/TableProMobile/TableProMobile/Views/ConnectionListView.swift index ac0b1faa4..861ac63e9 100644 --- a/TableProMobile/TableProMobile/Views/ConnectionListView.swift +++ b/TableProMobile/TableProMobile/Views/ConnectionListView.swift @@ -160,7 +160,7 @@ struct ConnectionListView: View { SettingsView() .toolbar { ToolbarItem(placement: .confirmationAction) { - Button(String(localized: "Done")) { + CloseButton { showingSettings = false } } diff --git a/TableProMobile/TableProMobile/Views/DataBrowserView.swift b/TableProMobile/TableProMobile/Views/DataBrowserView.swift index 31cd24b77..a86e7a980 100644 --- a/TableProMobile/TableProMobile/Views/DataBrowserView.swift +++ b/TableProMobile/TableProMobile/Views/DataBrowserView.swift @@ -151,7 +151,7 @@ struct DataBrowserView: View { Task { await viewModel.goToPage(page) } } } - Button("Cancel", role: .cancel) {} + CancelButton {} } message: { if let total = viewModel.pagination.totalRows { let totalPages = (total + viewModel.pagination.pageSize - 1) / viewModel.pagination.pageSize diff --git a/TableProMobile/TableProMobile/Views/GroupManagementView.swift b/TableProMobile/TableProMobile/Views/GroupManagementView.swift index c058713f1..be90e82cc 100644 --- a/TableProMobile/TableProMobile/Views/GroupManagementView.swift +++ b/TableProMobile/TableProMobile/Views/GroupManagementView.swift @@ -96,7 +96,7 @@ struct GroupManagementView: View { } label: { Image(systemName: "plus") } - Button("Done") { dismiss() } + CloseButton { dismiss() } } } .sheet(isPresented: $showingAddGroup) { diff --git a/TableProMobile/TableProMobile/Views/InsertRowView.swift b/TableProMobile/TableProMobile/Views/InsertRowView.swift index c1baf56ab..c8bd3bc9e 100644 --- a/TableProMobile/TableProMobile/Views/InsertRowView.swift +++ b/TableProMobile/TableProMobile/Views/InsertRowView.swift @@ -113,18 +113,18 @@ struct InsertRowView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { dismiss() } + CancelButton { dismiss() } .disabled(isSaving) } ToolbarItem(placement: .confirmationAction) { - Button { + ConfirmButton { Task { await insertRow() } } label: { if isSaving { ProgressView() .controlSize(.small) } else { - Text("Save") + Text("Done") } } .disabled(isSaving) @@ -145,7 +145,7 @@ struct InsertRowView: View { Button(String(localized: "Insert"), role: .destructive) { Task { await executePendingInsert() } } - Button(String(localized: "Cancel"), role: .cancel) {} + CancelButton {} } message: { Text(String(format: String(localized: "This will insert a row into %@. Continue?"), table.name)) } diff --git a/TableProMobile/TableProMobile/Views/RowDetailView.swift b/TableProMobile/TableProMobile/Views/RowDetailView.swift index 742cf7826..1e229ddf5 100644 --- a/TableProMobile/TableProMobile/Views/RowDetailView.swift +++ b/TableProMobile/TableProMobile/Views/RowDetailView.swift @@ -94,7 +94,7 @@ struct RowDetailView: View { Button(String(localized: "Save"), role: .destructive) { Task { await executePendingSave() } } - Button(String(localized: "Cancel"), role: .cancel) {} + CancelButton {} } message: { Text(String(format: String(localized: "This will update a row in %@. Continue?"), viewModel.table?.name ?? "")) } @@ -124,14 +124,14 @@ struct RowDetailView: View { ToolbarItem(placement: .primaryAction) { if viewModel.canEdit { if viewModel.isEditing { - Button { + ConfirmButton { Task { await handleSave() } } label: { if viewModel.isSaving { ProgressView() .controlSize(.small) } else { - Text("Save") + Text("Done") } } .disabled(viewModel.isSaving) @@ -143,7 +143,7 @@ struct RowDetailView: View { if viewModel.isEditing { ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { viewModel.cancelEditing() } + CancelButton { viewModel.cancelEditing() } .disabled(viewModel.isSaving) } } diff --git a/TableProMobile/TableProMobile/Views/SettingsView.swift b/TableProMobile/TableProMobile/Views/SettingsView.swift index d214f7f8a..a47fec029 100644 --- a/TableProMobile/TableProMobile/Views/SettingsView.swift +++ b/TableProMobile/TableProMobile/Views/SettingsView.swift @@ -121,7 +121,7 @@ struct SettingsView: View { Button(String(localized: "Refresh from iCloud")) { Task { await runRefresh() } } - Button(String(localized: "Cancel"), role: .cancel) {} + CancelButton {} } message: { Text("TablePro will re-download every connection, group, and tag from your iCloud account. Local data on this device is not deleted.") } diff --git a/TableProMobile/TableProMobile/Views/TagManagementView.swift b/TableProMobile/TableProMobile/Views/TagManagementView.swift index 7fd3ce153..51ec3b696 100644 --- a/TableProMobile/TableProMobile/Views/TagManagementView.swift +++ b/TableProMobile/TableProMobile/Views/TagManagementView.swift @@ -74,7 +74,7 @@ struct TagManagementView: View { } label: { Image(systemName: "plus") } - Button("Done") { dismiss() } + CloseButton { dismiss() } } } .sheet(isPresented: $showingAddTag) { From 39df9fa38a5a1771d6dfc5f7e86abe181f60acf6 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 31 May 2026 22:23:20 +0700 Subject: [PATCH 2/2] refactor(ios): restore confirm labels and in-progress spinner for sheet buttons --- CHANGELOG.md | 1 + .../Views/Components/FilterSheetView.swift | 4 +--- .../Views/Components/GroupFormSheet.swift | 4 +--- .../Views/Components/SemanticButtons.swift | 19 +++++++++---------- .../Views/Components/TagFormSheet.swift | 4 +--- .../TableProMobile/Views/ConnectedView.swift | 2 +- .../Views/ConnectionFormView.swift | 4 ++-- .../Views/DataBrowserView.swift | 2 +- .../TableProMobile/Views/InsertRowView.swift | 11 ++--------- .../TableProMobile/Views/RowDetailView.swift | 11 ++--------- .../TableProMobile/Views/SettingsView.swift | 2 +- 11 files changed, 22 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2044a24a9..6fa8aeabd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Destructive queries (DROP, TRUNCATE, DELETE without WHERE) now always ask for confirmation, even with Safe Mode off. (#1481) - Table structure changes, table creation, maintenance, column reorder, and saved data-grid edits now follow the connection's Safe Mode and read-only setting. (#1481) - AI assistant and MCP queries now follow the same Safe Mode confirmation, read-only, and authentication rules as the editor. (#1481) +- iOS: sheet close, cancel, and confirm buttons use the native iOS 26 button roles, matching system apps like Mail. iOS 18 keeps titled buttons. (#1524) ### Removed diff --git a/TableProMobile/TableProMobile/Views/Components/FilterSheetView.swift b/TableProMobile/TableProMobile/Views/Components/FilterSheetView.swift index 46ba24378..c5693e3c4 100644 --- a/TableProMobile/TableProMobile/Views/Components/FilterSheetView.swift +++ b/TableProMobile/TableProMobile/Views/Components/FilterSheetView.swift @@ -91,13 +91,11 @@ struct FilterSheetView: View { CancelButton { dismiss() } } ToolbarItem(placement: .confirmationAction) { - ConfirmButton { + ConfirmButton(title: "Apply") { filters = draft logicMode = draftLogicMode onApply() dismiss() - } label: { - Text("Done") } .disabled(!hasValidFilters) } diff --git a/TableProMobile/TableProMobile/Views/Components/GroupFormSheet.swift b/TableProMobile/TableProMobile/Views/Components/GroupFormSheet.swift index c4da7594f..15758a8d4 100644 --- a/TableProMobile/TableProMobile/Views/Components/GroupFormSheet.swift +++ b/TableProMobile/TableProMobile/Views/Components/GroupFormSheet.swift @@ -35,14 +35,12 @@ struct GroupFormSheet: View { CancelButton { dismiss() } } ToolbarItem(placement: .confirmationAction) { - ConfirmButton { + ConfirmButton(title: "Save") { var group = existingGroup ?? ConnectionGroup() group.name = name group.color = color onSave(group) dismiss() - } label: { - Text("Done") } .disabled(name.trimmingCharacters(in: .whitespaces).isEmpty) } diff --git a/TableProMobile/TableProMobile/Views/Components/SemanticButtons.swift b/TableProMobile/TableProMobile/Views/Components/SemanticButtons.swift index 827b41b74..ae6735f44 100644 --- a/TableProMobile/TableProMobile/Views/Components/SemanticButtons.swift +++ b/TableProMobile/TableProMobile/Views/Components/SemanticButtons.swift @@ -24,20 +24,19 @@ struct CancelButton: View { } } -struct ConfirmButton: View { - private let action: () -> Void - private let label: Label - - init(action: @escaping () -> Void, @ViewBuilder label: () -> Label) { - self.action = action - self.label = label() - } +struct ConfirmButton: View { + let title: LocalizedStringKey + var isInProgress = false + let action: () -> Void var body: some View { - if #available(iOS 26.0, *) { + if isInProgress { + ProgressView() + .controlSize(.small) + } else if #available(iOS 26.0, *) { Button(role: .confirm, action: action) } else { - Button(action: action) { label } + Button(title, action: action) } } } diff --git a/TableProMobile/TableProMobile/Views/Components/TagFormSheet.swift b/TableProMobile/TableProMobile/Views/Components/TagFormSheet.swift index a7989bb6c..202ae722a 100644 --- a/TableProMobile/TableProMobile/Views/Components/TagFormSheet.swift +++ b/TableProMobile/TableProMobile/Views/Components/TagFormSheet.swift @@ -35,14 +35,12 @@ struct TagFormSheet: View { CancelButton { dismiss() } } ToolbarItem(placement: .confirmationAction) { - ConfirmButton { + ConfirmButton(title: "Save") { var tag = existingTag ?? ConnectionTag() tag.name = name tag.color = color onSave(tag) dismiss() - } label: { - Text("Done") } .disabled(name.trimmingCharacters(in: .whitespaces).isEmpty) } diff --git a/TableProMobile/TableProMobile/Views/ConnectedView.swift b/TableProMobile/TableProMobile/Views/ConnectedView.swift index 313b9ceec..884516866 100644 --- a/TableProMobile/TableProMobile/Views/ConnectedView.swift +++ b/TableProMobile/TableProMobile/Views/ConnectedView.swift @@ -82,7 +82,7 @@ struct ConnectedView: View { Text(String(format: String(localized: "Connecting to %@..."), connection.name.isEmpty ? connection.host : connection.name)) } - CancelButton { + Button(String(localized: "Cancel"), role: .cancel) { dismiss() } .buttonStyle(.bordered) diff --git a/TableProMobile/TableProMobile/Views/ConnectionFormView.swift b/TableProMobile/TableProMobile/Views/ConnectionFormView.swift index b7c2c8780..484202960 100644 --- a/TableProMobile/TableProMobile/Views/ConnectionFormView.swift +++ b/TableProMobile/TableProMobile/Views/ConnectionFormView.swift @@ -84,7 +84,7 @@ struct ConnectionFormView: View { CancelButton { dismiss() } } ToolbarItem(placement: .confirmationAction) { - ConfirmButton(action: handleSave) { Text("Done") } + ConfirmButton(title: "Save", action: handleSave) .disabled(!viewModel.canSave) } } @@ -104,7 +104,7 @@ struct ConnectionFormView: View { .alert("New Database", isPresented: $showNewDatabaseAlert) { TextField("Database name", text: $viewModel.newDatabaseName) Button("Create") { viewModel.createNewDatabase() } - CancelButton { viewModel.newDatabaseName = "" } + Button("Cancel", role: .cancel) { viewModel.newDatabaseName = "" } } message: { Text("Enter a name for the new SQLite database.") } diff --git a/TableProMobile/TableProMobile/Views/DataBrowserView.swift b/TableProMobile/TableProMobile/Views/DataBrowserView.swift index a86e7a980..31cd24b77 100644 --- a/TableProMobile/TableProMobile/Views/DataBrowserView.swift +++ b/TableProMobile/TableProMobile/Views/DataBrowserView.swift @@ -151,7 +151,7 @@ struct DataBrowserView: View { Task { await viewModel.goToPage(page) } } } - CancelButton {} + Button("Cancel", role: .cancel) {} } message: { if let total = viewModel.pagination.totalRows { let totalPages = (total + viewModel.pagination.pageSize - 1) / viewModel.pagination.pageSize diff --git a/TableProMobile/TableProMobile/Views/InsertRowView.swift b/TableProMobile/TableProMobile/Views/InsertRowView.swift index c8bd3bc9e..0b3465260 100644 --- a/TableProMobile/TableProMobile/Views/InsertRowView.swift +++ b/TableProMobile/TableProMobile/Views/InsertRowView.swift @@ -117,15 +117,8 @@ struct InsertRowView: View { .disabled(isSaving) } ToolbarItem(placement: .confirmationAction) { - ConfirmButton { + ConfirmButton(title: "Save", isInProgress: isSaving) { Task { await insertRow() } - } label: { - if isSaving { - ProgressView() - .controlSize(.small) - } else { - Text("Done") - } } .disabled(isSaving) } @@ -145,7 +138,7 @@ struct InsertRowView: View { Button(String(localized: "Insert"), role: .destructive) { Task { await executePendingInsert() } } - CancelButton {} + Button(String(localized: "Cancel"), role: .cancel) {} } message: { Text(String(format: String(localized: "This will insert a row into %@. Continue?"), table.name)) } diff --git a/TableProMobile/TableProMobile/Views/RowDetailView.swift b/TableProMobile/TableProMobile/Views/RowDetailView.swift index 1e229ddf5..250965931 100644 --- a/TableProMobile/TableProMobile/Views/RowDetailView.swift +++ b/TableProMobile/TableProMobile/Views/RowDetailView.swift @@ -94,7 +94,7 @@ struct RowDetailView: View { Button(String(localized: "Save"), role: .destructive) { Task { await executePendingSave() } } - CancelButton {} + Button(String(localized: "Cancel"), role: .cancel) {} } message: { Text(String(format: String(localized: "This will update a row in %@. Continue?"), viewModel.table?.name ?? "")) } @@ -124,15 +124,8 @@ struct RowDetailView: View { ToolbarItem(placement: .primaryAction) { if viewModel.canEdit { if viewModel.isEditing { - ConfirmButton { + ConfirmButton(title: "Save", isInProgress: viewModel.isSaving) { Task { await handleSave() } - } label: { - if viewModel.isSaving { - ProgressView() - .controlSize(.small) - } else { - Text("Done") - } } .disabled(viewModel.isSaving) } else { diff --git a/TableProMobile/TableProMobile/Views/SettingsView.swift b/TableProMobile/TableProMobile/Views/SettingsView.swift index a47fec029..d214f7f8a 100644 --- a/TableProMobile/TableProMobile/Views/SettingsView.swift +++ b/TableProMobile/TableProMobile/Views/SettingsView.swift @@ -121,7 +121,7 @@ struct SettingsView: View { Button(String(localized: "Refresh from iCloud")) { Task { await runRefresh() } } - CancelButton {} + Button(String(localized: "Cancel"), role: .cancel) {} } message: { Text("TablePro will re-download every connection, group, and tag from your iCloud account. Local data on this device is not deleted.") }