Skip to content

Commit 6ccff6b

Browse files
committed
fix: 14 iOS HIG fixes — toolbar placement, alert messages, accessibility, native patterns
1 parent adf0ad8 commit 6ccff6b

File tree

10 files changed

+112
-105
lines changed

10 files changed

+112
-105
lines changed

TableProMobile/TableProMobile/Views/Components/ConnectionColorPicker.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ struct ConnectionColorPicker: View {
3131
.contentShape(Circle())
3232
}
3333
.buttonStyle(.plain)
34+
.accessibilityLabel(Text(color.rawValue))
3435
}
3536
}
3637
.padding(.vertical, 4)

TableProMobile/TableProMobile/Views/ConnectedView.swift

Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ struct ConnectedView: View {
1919
@State private var tables: [TableInfo] = []
2020
@State private var isConnecting = true
2121
@State private var appError: AppError?
22-
@State private var toastMessage: String?
23-
@State private var toastTask: Task<Void, Never>?
22+
@State private var failureAlertMessage: String?
23+
@State private var showFailureAlert = false
2424
@State private var selectedTab = ConnectedTab.tables
2525
@State private var queryHistory: [QueryHistoryItem] = []
2626
private let historyStorage = QueryHistoryStorage()
@@ -77,28 +77,24 @@ struct ConnectedView: View {
7777
.animation(.default, value: isSwitching)
7878
}
7979
}
80-
.overlay(alignment: .bottom) {
81-
if let toastMessage {
82-
ErrorToast(message: toastMessage)
83-
.onAppear {
84-
toastTask?.cancel()
85-
toastTask = Task {
86-
try? await Task.sleep(nanoseconds: 3_000_000_000)
87-
withAnimation { self.toastMessage = nil }
88-
}
89-
}
90-
.onDisappear {
91-
toastTask?.cancel()
92-
toastTask = nil
93-
}
94-
}
80+
.alert("Error", isPresented: $showFailureAlert) {
81+
Button("OK", role: .cancel) {}
82+
} message: {
83+
Text(failureAlertMessage ?? "")
9584
}
96-
.animation(.default, value: toastMessage)
9785
.navigationTitle(displayName)
9886
.navigationBarTitleDisplayMode(.inline)
9987
.toolbar {
88+
ToolbarItem(placement: .principal) {
89+
Picker("Tab", selection: $selectedTab) {
90+
Text("Tables").tag(ConnectedTab.tables)
91+
Text("Query").tag(ConnectedTab.query)
92+
}
93+
.pickerStyle(.segmented)
94+
.frame(width: 200)
95+
}
10096
if supportsDatabaseSwitching && databases.count > 1 {
101-
ToolbarItem(placement: .principal) {
97+
ToolbarItem(placement: .topBarLeading) {
10298
Menu {
10399
ForEach(databases, id: \.self) { db in
104100
Button {
@@ -114,7 +110,7 @@ struct ConnectedView: View {
114110
} label: {
115111
HStack(spacing: 4) {
116112
Text(activeDatabase)
117-
.font(.headline)
113+
.font(.subheadline)
118114
if isSwitching {
119115
ProgressView()
120116
.controlSize(.mini)
@@ -163,15 +159,6 @@ struct ConnectedView: View {
163159

164160
private var connectedContent: some View {
165161
VStack(spacing: 0) {
166-
Picker("Tab", selection: $selectedTab) {
167-
ForEach(ConnectedTab.allCases, id: \.self) { tab in
168-
Text(tab.rawValue).tag(tab)
169-
}
170-
}
171-
.pickerStyle(.segmented)
172-
.padding(.horizontal)
173-
.padding(.vertical, 8)
174-
175162
switch selectedTab {
176163
case .tables:
177164
TableListView(
@@ -301,7 +288,8 @@ struct ConnectedView: View {
301288
activeSchema = name
302289
self.tables = try await session.driver.fetchTables(schema: name)
303290
} catch {
304-
withAnimation { toastMessage = String(localized: "Failed to switch schema") }
291+
failureAlertMessage = String(localized: "Failed to switch schema")
292+
showFailureAlert = true
305293
}
306294
}
307295

@@ -318,7 +306,8 @@ struct ConnectedView: View {
318306
activeDatabase = name
319307
self.tables = try await session.driver.fetchTables(schema: nil)
320308
} catch {
321-
withAnimation { toastMessage = String(localized: "Failed to switch database") }
309+
failureAlertMessage = String(localized: "Failed to switch database")
310+
showFailureAlert = true
322311
}
323312
}
324313
}
@@ -346,7 +335,8 @@ struct ConnectedView: View {
346335
let fallbackSession = try await appState.connectionManager.connect(connection)
347336
self.session = fallbackSession
348337
self.tables = try await fallbackSession.driver.fetchTables(schema: nil)
349-
withAnimation { toastMessage = String(localized: "Failed to switch database") }
338+
failureAlertMessage = String(localized: "Failed to switch database")
339+
showFailureAlert = true
350340
} catch {
351341
// Both failed — show error view
352342
let context = ErrorContext(
@@ -368,7 +358,8 @@ struct ConnectedView: View {
368358
self.tables = try await session.driver.fetchTables(schema: schema)
369359
} catch {
370360
Self.logger.warning("Failed to refresh tables: \(error.localizedDescription, privacy: .public)")
371-
withAnimation { toastMessage = String(localized: "Failed to refresh tables") }
361+
failureAlertMessage = String(localized: "Failed to refresh tables")
362+
showFailureAlert = true
372363
}
373364
}
374365
}

TableProMobile/TableProMobile/Views/DataBrowserView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ struct DataBrowserView: View {
178178
}
179179
}
180180
}
181-
.listStyle(.insetGrouped)
181+
.listStyle(.plain)
182182
.opacity(isPageLoading ? 0.5 : 1)
183183
.allowsHitTesting(!isPageLoading)
184184
.overlay { if isPageLoading { ProgressView() } }

TableProMobile/TableProMobile/Views/GroupManagementView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ struct GroupManagementView: View {
6464
.navigationTitle("Groups")
6565
.navigationBarTitleDisplayMode(.inline)
6666
.toolbar {
67-
ToolbarItem(placement: .topBarLeading) {
67+
ToolbarItem(placement: .confirmationAction) {
6868
Button("Done") { dismiss() }
6969
}
7070
ToolbarItemGroup(placement: .topBarTrailing) {

TableProMobile/TableProMobile/Views/InsertRowView.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,10 @@ struct InsertRowView: View {
136136
.alert(operationError?.title ?? "Error", isPresented: $showOperationError) {
137137
Button("OK", role: .cancel) {}
138138
} message: {
139-
VStack {
140-
Text(operationError?.message ?? "An unknown error occurred.")
141-
if let recovery = operationError?.recovery {
142-
Text(verbatim: recovery)
143-
}
139+
if let recovery = operationError?.recovery {
140+
Text("\(operationError?.message ?? "")\n\(recovery)")
141+
} else {
142+
Text(operationError?.message ?? "")
144143
}
145144
}
146145
}

TableProMobile/TableProMobile/Views/OnboardingView.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ struct OnboardingView: View {
8282
description: String(localized: "Import connections from your Mac")
8383
)
8484
}
85-
.buttonStyle(.plain)
8685

8786
Button(action: addNewConnection) {
8887
actionCard(
@@ -92,7 +91,6 @@ struct OnboardingView: View {
9291
description: String(localized: "Set up a new database connection")
9392
)
9493
}
95-
.buttonStyle(.plain)
9694
}
9795
.padding(.horizontal, 24)
9896

TableProMobile/TableProMobile/Views/QueryEditorView.swift

Lines changed: 73 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ struct QueryEditorView: View {
2525
let connectionId: UUID
2626
let historyStorage: QueryHistoryStorage
2727
@State private var showHistory = false
28+
@State private var showClearHistoryConfirmation = false
2829
@FocusState private var editorFocused: Bool
2930

3031
var body: some View {
@@ -134,58 +135,70 @@ struct QueryEditorView: View {
134135
}
135136
}
136137

137-
// Native iOS pattern: List with rows, each row shows column:value pairs
138138
private func resultList(_ result: QueryResult) -> some View {
139139
List {
140140
ForEach(Array(result.rows.enumerated()), id: \.offset) { rowIndex, row in
141-
Section {
142-
ForEach(Array(result.columns.enumerated()), id: \.offset) { colIndex, col in
143-
let value = colIndex < row.count ? row[colIndex] : nil
144-
HStack(alignment: .top) {
145-
Text(verbatim: col.name)
146-
.font(.caption)
147-
.foregroundStyle(.secondary)
148-
.frame(width: 90, alignment: .leading)
149-
Text(verbatim: value ?? "NULL")
150-
.font(.system(.body, design: .monospaced))
151-
.foregroundStyle(value == nil ? .secondary : .primary)
152-
.frame(maxWidth: .infinity, alignment: .leading)
153-
.lineLimit(3)
154-
.textSelection(.enabled)
155-
}
156-
.contextMenu {
157-
if let value {
158-
Button {
159-
UIPasteboard.general.string = value
160-
} label: {
161-
Label("Copy Value", systemImage: "doc.on.doc")
162-
}
163-
}
164-
Button {
165-
UIPasteboard.general.string = col.name
166-
} label: {
167-
Label("Copy Column Name", systemImage: "textformat")
168-
}
169-
Divider()
170-
Menu("Copy Row") {
171-
ForEach(ExportFormat.allCases) { format in
172-
Button(format.rawValue) {
173-
let text = ClipboardExporter.exportRow(
174-
columns: result.columns, row: row,
175-
format: format
176-
)
177-
ClipboardExporter.copyToClipboard(text)
178-
}
179-
}
180-
}
181-
}
182-
}
183-
} header: {
184-
Text(verbatim: "Row \(rowIndex + 1)")
141+
NavigationLink {
142+
RowDetailView(
143+
columns: result.columns,
144+
rows: result.rows,
145+
initialIndex: rowIndex
146+
)
147+
} label: {
148+
resultRowCard(columns: result.columns, row: row)
149+
}
150+
.contextMenu {
151+
resultRowContextMenu(columns: result.columns, row: row)
152+
}
153+
}
154+
}
155+
.listStyle(.plain)
156+
}
157+
158+
private func resultRowCard(columns: [ColumnInfo], row: [String?]) -> some View {
159+
let preview = Array(zip(columns, row).prefix(4))
160+
return VStack(alignment: .leading, spacing: 4) {
161+
ForEach(Array(preview.enumerated()), id: \.offset) { index, pair in
162+
HStack(spacing: 6) {
163+
Text(pair.0.name)
164+
.font(.caption2)
165+
.foregroundStyle(.tertiary)
166+
Text(verbatim: pair.1 ?? "NULL")
167+
.font(index == 0 ? .subheadline : .caption)
168+
.fontWeight(index == 0 ? .medium : .regular)
169+
.foregroundStyle(pair.1 == nil ? .secondary : .primary)
170+
.lineLimit(1)
171+
}
172+
}
173+
if columns.count > 4 {
174+
Text("+\(columns.count - 4) more columns")
175+
.font(.caption2)
176+
.foregroundStyle(.quaternary)
177+
}
178+
}
179+
.padding(.vertical, 2)
180+
}
181+
182+
@ViewBuilder
183+
private func resultRowContextMenu(columns: [ColumnInfo], row: [String?]) -> some View {
184+
if let firstValue = row.first, let value = firstValue {
185+
Button {
186+
UIPasteboard.general.string = value
187+
} label: {
188+
Label("Copy Value", systemImage: "doc.on.doc")
189+
}
190+
}
191+
Menu("Copy Row") {
192+
ForEach(ExportFormat.allCases) { format in
193+
Button(format.rawValue) {
194+
let text = ClipboardExporter.exportRow(
195+
columns: columns, row: row,
196+
format: format
197+
)
198+
ClipboardExporter.copyToClipboard(text)
185199
}
186200
}
187201
}
188-
.listStyle(.insetGrouped)
189202
}
190203

191204
// MARK: - Toolbar
@@ -287,23 +300,29 @@ struct QueryEditorView: View {
287300
}
288301
queryHistory = historyStorage.load(for: connectionId)
289302
}
303+
304+
if !queryHistory.isEmpty {
305+
Section {
306+
Button("Clear All History", role: .destructive) {
307+
showClearHistoryConfirmation = true
308+
}
309+
}
310+
}
290311
}
291312
.listStyle(.insetGrouped)
292313
.navigationTitle("Query History")
293314
.navigationBarTitleDisplayMode(.inline)
294315
.toolbar {
295-
ToolbarItem(placement: .cancellationAction) {
296-
if !queryHistory.isEmpty {
297-
Button("Clear All", role: .destructive) {
298-
historyStorage.clearAll(for: connectionId)
299-
queryHistory = []
300-
}
301-
}
302-
}
303316
ToolbarItem(placement: .confirmationAction) {
304317
Button("Done") { showHistory = false }
305318
}
306319
}
320+
.confirmationDialog("Clear History", isPresented: $showClearHistoryConfirmation) {
321+
Button("Clear All", role: .destructive) {
322+
historyStorage.clearAll(for: connectionId)
323+
queryHistory = []
324+
}
325+
}
307326
.overlay {
308327
if queryHistory.isEmpty {
309328
ContentUnavailableView(

TableProMobile/TableProMobile/Views/RowDetailView.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,10 @@ struct RowDetailView: View {
204204
.alert(operationError?.title ?? "Error", isPresented: $showOperationError) {
205205
Button("OK", role: .cancel) {}
206206
} message: {
207-
VStack {
208-
Text(operationError?.message ?? "An unknown error occurred.")
209-
if let recovery = operationError?.recovery {
210-
Text(verbatim: recovery)
211-
}
207+
if let recovery = operationError?.recovery {
208+
Text("\(operationError?.message ?? "")\n\(recovery)")
209+
} else {
210+
Text(operationError?.message ?? "")
212211
}
213212
}
214213
}

TableProMobile/TableProMobile/Views/StructureView.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,9 @@ struct StructureView: View {
236236
guard let session else {
237237
appError = AppError(
238238
category: .config,
239-
title: "Not Connected",
240-
message: "No active database session.",
241-
recovery: "Go back and reconnect to the database.",
239+
title: String(localized: "Not Connected"),
240+
message: String(localized: "No active database session."),
241+
recovery: String(localized: "Go back and reconnect to the database."),
242242
underlying: nil
243243
)
244244
isLoading = false

TableProMobile/TableProMobile/Views/TagManagementView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ struct TagManagementView: View {
6666
.navigationTitle("Tags")
6767
.navigationBarTitleDisplayMode(.inline)
6868
.toolbar {
69-
ToolbarItem(placement: .topBarLeading) {
69+
ToolbarItem(placement: .confirmationAction) {
7070
Button("Done") { dismiss() }
7171
}
7272
ToolbarItemGroup(placement: .topBarTrailing) {

0 commit comments

Comments
 (0)