Skip to content

Commit 5b3e81e

Browse files
committed
feat(ios): add state restoration across app lifecycle
1 parent 1bb338c commit 5b3e81e

5 files changed

Lines changed: 52 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- iPad keyboard shortcuts (Cmd+N new connection, Cmd+Return execute query, Cmd+1/2 switch tabs) and trackpad hover effects on list rows
1414
- Server Dashboard with active sessions, server metrics, and slow query monitoring (PostgreSQL, MySQL, MSSQL, ClickHouse, DuckDB, SQLite)
1515
- Handoff support for cross-device continuity between iOS and macOS
16+
- State restoration across app lifecycle on iOS (selected connection, active tab, query text, database/schema selection)
1617

1718
## [0.30.1] - 2026-04-10
1819

TableProMobile/TableProMobile/TableProMobileApp.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import TableProModels
1010

1111
@main
1212
struct TableProMobileApp: App {
13+
private static let logger = Logger(subsystem: "com.TablePro", category: "App")
1314
@State private var appState = AppState()
1415
@State private var syncTask: Task<Void, Never>?
1516
@Environment(\.scenePhase) private var scenePhase
@@ -18,6 +19,7 @@ struct TableProMobileApp: App {
1819
WindowGroup {
1920
Group {
2021
if appState.hasCompletedOnboarding {
22+
let _ = Self.logger.info("App.body — lastConnectionId=\(UserDefaults.standard.string(forKey: "lastConnectionId") ?? "nil") lastTab=\(UserDefaults.standard.string(forKey: "lastSelectedTab") ?? "nil") lastQuery=\(UserDefaults.standard.string(forKey: "lastQueryText") ?? "nil")")
2123
ConnectionListView()
2224
.environment(appState)
2325
} else {

TableProMobile/TableProMobile/Views/ConnectedView.swift

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ struct ConnectedView: View {
2121
@State private var appError: AppError?
2222
@State private var failureAlertMessage: String?
2323
@State private var showFailureAlert = false
24-
@State private var selectedTab = ConnectedTab.tables
24+
@AppStorage("lastSelectedTab") private var selectedTabRaw: String = ConnectedTab.tables.rawValue
2525
@State private var queryHistory: [QueryHistoryItem] = []
2626
@State private var historyStorage = QueryHistoryStorage()
2727
@State private var databases: [String] = []
28-
@State private var activeDatabase: String = ""
28+
@AppStorage("lastActiveDatabase") private var activeDatabase: String = ""
2929
@State private var schemas: [String] = []
30-
@State private var activeSchema: String = "public"
30+
@AppStorage("lastActiveSchema") private var activeSchema: String = "public"
3131
@State private var isSwitching = false
3232
@State private var isReconnecting = false
3333
@State private var hapticSuccess = false
@@ -40,6 +40,18 @@ struct ConnectedView: View {
4040
case query = "Query"
4141
}
4242

43+
private var selectedTab: ConnectedTab {
44+
get { ConnectedTab(rawValue: selectedTabRaw) ?? .tables }
45+
set { selectedTabRaw = newValue.rawValue }
46+
}
47+
48+
private var selectedTabBinding: Binding<ConnectedTab> {
49+
Binding(
50+
get: { ConnectedTab(rawValue: selectedTabRaw) ?? .tables },
51+
set: { selectedTabRaw = $0.rawValue }
52+
)
53+
}
54+
4355
private var displayName: String {
4456
connection.name.isEmpty ? connection.host : connection.name
4557
}
@@ -119,7 +131,7 @@ struct ConnectedView: View {
119131
.navigationTitle(supportsDatabaseSwitching && databases.count > 1 ? "" : displayName)
120132
.navigationBarTitleDisplayMode(.inline)
121133
.safeAreaInset(edge: .top) {
122-
Picker("Tab", selection: $selectedTab) {
134+
Picker("Tab", selection: selectedTabBinding) {
123135
Text("Tables").tag(ConnectedTab.tables)
124136
Text("Query").tag(ConnectedTab.query)
125137
}

TableProMobile/TableProMobile/Views/ConnectionListView.swift

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,22 @@
44
//
55

66
import SwiftUI
7+
import os
78
import TableProModels
89
import TableProSync
910

1011
struct ConnectionListView: View {
12+
private static let logger = Logger(subsystem: "com.TablePro", category: "ConnectionListView")
1113
@Environment(AppState.self) private var appState
1214
@Environment(\.horizontalSizeClass) private var sizeClass
1315
@State private var showingAddConnection = false
1416
@State private var editingConnection: DatabaseConnection?
15-
@State private var selectedConnectionId: UUID?
17+
@AppStorage("lastConnectionId") private var selectedConnectionIdString: String?
1618
@State private var columnVisibility: NavigationSplitViewVisibility = .automatic
1719
@State private var showingGroupManagement = false
1820
@State private var showingTagManagement = false
19-
@State private var filterTagId: UUID?
20-
@State private var groupByGroup = false
21+
@AppStorage("lastFilterTagId") private var filterTagIdString: String?
22+
@AppStorage("groupByGroup") private var groupByGroup = false
2123
@State private var editMode: EditMode = .inactive
2224
@State private var connectionToDelete: DatabaseConnection?
2325

@@ -28,6 +30,21 @@ struct ConnectionListView: View {
2830
)
2931
}
3032

33+
private var selectedConnectionId: Binding<UUID?> {
34+
Binding(
35+
get: { selectedConnectionIdString.flatMap { UUID(uuidString: $0) } },
36+
set: { selectedConnectionIdString = $0?.uuidString }
37+
)
38+
}
39+
40+
private var selectedConnectionUUID: UUID? {
41+
selectedConnectionIdString.flatMap { UUID(uuidString: $0) }
42+
}
43+
44+
private var filterTagId: UUID? {
45+
filterTagIdString.flatMap { UUID(uuidString: $0) }
46+
}
47+
3148
private var displayedConnections: [DatabaseConnection] {
3249
var result = appState.connections
3350
if let filterTagId {
@@ -44,11 +61,12 @@ struct ConnectionListView: View {
4461
}
4562

4663
private var selectedConnection: DatabaseConnection? {
47-
guard let selectedConnectionId else { return nil }
48-
return appState.connections.first { $0.id == selectedConnectionId }
64+
guard let id = selectedConnectionUUID else { return nil }
65+
return appState.connections.first { $0.id == id }
4966
}
5067

5168
var body: some View {
69+
let _ = Self.logger.info("ConnectionListView.body — selectedConnectionIdString=\(selectedConnectionIdString ?? "nil") selectedConnectionUUID=\(selectedConnectionUUID?.uuidString ?? "nil") selectedConnection=\(selectedConnection?.name ?? "nil") connections.count=\(appState.connections.count)")
5270
NavigationSplitView(columnVisibility: $columnVisibility) {
5371
sidebar
5472
.navigationTitle("Connections")
@@ -90,7 +108,7 @@ struct ConnectionListView: View {
90108
.onChange(of: appState.pendingConnectionId) { _, newId in
91109
navigateToPendingConnection(newId)
92110
}
93-
.onChange(of: filterTagId) {
111+
.onChange(of: filterTagIdString) {
94112
editMode = .inactive
95113
}
96114
.onChange(of: groupByGroup) {
@@ -102,9 +120,11 @@ struct ConnectionListView: View {
102120
} detail: {
103121
NavigationStack {
104122
if let connection = selectedConnection {
123+
let _ = Self.logger.info("detail: showing ConnectedView for \(connection.name)")
105124
ConnectedView(connection: connection)
106125
.id(connection.id)
107126
} else {
127+
let _ = Self.logger.info("detail: no selectedConnection, showing empty state")
108128
ContentUnavailableView(
109129
"Select a Connection",
110130
systemImage: "server.rack",
@@ -135,7 +155,7 @@ struct ConnectionListView: View {
135155

136156
@ViewBuilder
137157
private var connectionList: some View {
138-
let list = List(selection: $selectedConnectionId) {
158+
let list = List(selection: selectedConnectionId) {
139159
if groupByGroup {
140160
groupedContent
141161
} else {
@@ -201,8 +221,8 @@ struct ConnectionListView: View {
201221
) {
202222
Button(String(localized: "Delete"), role: .destructive) {
203223
if let connection = connectionToDelete {
204-
if selectedConnectionId == connection.id {
205-
selectedConnectionId = nil
224+
if selectedConnectionUUID == connection.id {
225+
selectedConnectionIdString = nil
206226
}
207227
appState.removeConnection(connection)
208228
}
@@ -222,7 +242,7 @@ struct ConnectionListView: View {
222242
if !appState.tags.isEmpty {
223243
Section("Filter by Tag") {
224244
Button {
225-
filterTagId = nil
245+
filterTagIdString = nil
226246
} label: {
227247
HStack {
228248
Text("All")
@@ -233,7 +253,7 @@ struct ConnectionListView: View {
233253
}
234254
ForEach(appState.tags) { tag in
235255
Button {
236-
filterTagId = tag.id
256+
filterTagIdString = tag.id.uuidString
237257
} label: {
238258
HStack {
239259
Image(systemName: "circle.fill")
@@ -339,7 +359,7 @@ struct ConnectionListView: View {
339359
private func navigateToPendingConnection(_ id: UUID?) {
340360
guard let id,
341361
appState.connections.contains(where: { $0.id == id }) else { return }
342-
selectedConnectionId = id
362+
selectedConnectionIdString = id.uuidString
343363
appState.pendingConnectionId = nil
344364
}
345365

TableProMobile/TableProMobile/Views/QueryEditorView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ struct QueryEditorView: View {
1717

1818
private static let logger = Logger(subsystem: "com.TablePro", category: "QueryEditorView")
1919

20-
@State private var query = ""
20+
@AppStorage("lastQueryText") private var query = ""
2121
@State private var result: QueryResult?
2222
@State private var appError: AppError?
2323
@State private var isExecuting = false

0 commit comments

Comments
 (0)