Kill the NavigationStack on the list — that was the floating-chrome bug
iOS 26's NavigationStack reserves space for its floating Liquid Glass nav bar even with `.toolbar(.hidden, for: .navigationBar)` — that's why "Screens" landed ~180pt down from the dynamic island with a wedge of black above it. Removed the NavigationStack from the list entirely. Layout is now a plain top-anchored VStack: top chrome (gear + Screens + plus + search) flush with the safe-area top, then a ScrollView with LazyVStack of cards filling the rest of the screen. Sessions present via fullScreenCover instead of NavigationLink, so we don't need NavigationStack here at all. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,67 +6,45 @@ public struct ConnectionListView: View {
|
|||||||
@Environment(\.modelContext) private var modelContext
|
@Environment(\.modelContext) private var modelContext
|
||||||
@Environment(\.openWindow) private var openWindow
|
@Environment(\.openWindow) private var openWindow
|
||||||
@Query(sort: \SavedConnection.displayName) private var connections: [SavedConnection]
|
@Query(sort: \SavedConnection.displayName) private var connections: [SavedConnection]
|
||||||
|
|
||||||
@State private var discovery = DiscoveryService()
|
@State private var discovery = DiscoveryService()
|
||||||
@State private var showingAdd = false
|
@State private var showingAdd = false
|
||||||
@State private var showingSettings = false
|
@State private var showingSettings = false
|
||||||
@State private var addPrefill: AddConnectionPrefill?
|
@State private var addPrefill: AddConnectionPrefill?
|
||||||
@State private var editingConnection: SavedConnection?
|
@State private var editingConnection: SavedConnection?
|
||||||
@State private var path: [SessionRoute] = []
|
@State private var sessionConnection: SavedConnection?
|
||||||
@State private var resolvingHostID: String?
|
@State private var resolvingHostID: String?
|
||||||
@State private var search = ""
|
@State private var search = ""
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
NavigationStack(path: $path) {
|
VStack(spacing: 0) {
|
||||||
ZStack(alignment: .top) {
|
topChrome
|
||||||
backgroundGradient.ignoresSafeArea()
|
contentList
|
||||||
|
|
||||||
ScrollView {
|
|
||||||
VStack(spacing: 22) {
|
|
||||||
topChrome
|
|
||||||
|
|
||||||
if !discovery.hosts.isEmpty || discovery.isBrowsing {
|
|
||||||
discoveredSection
|
|
||||||
}
|
|
||||||
|
|
||||||
savedSection
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.padding(.top, 8)
|
|
||||||
.padding(.bottom, 32)
|
|
||||||
}
|
|
||||||
#if os(iOS)
|
|
||||||
.scrollIndicators(.hidden)
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
#if os(iOS)
|
|
||||||
.toolbar(.hidden, for: .navigationBar)
|
|
||||||
#endif
|
|
||||||
.sheet(isPresented: $showingAdd) {
|
|
||||||
AddConnectionView(prefill: addPrefill)
|
|
||||||
}
|
|
||||||
.sheet(item: $editingConnection) { connection in
|
|
||||||
AddConnectionView(editing: connection)
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showingSettings) {
|
|
||||||
SettingsView()
|
|
||||||
}
|
|
||||||
.navigationDestination(for: SessionRoute.self) { route in
|
|
||||||
if let connection = connection(with: route.connectionID) {
|
|
||||||
SessionView(connection: connection)
|
|
||||||
} else {
|
|
||||||
ContentUnavailableView("Connection unavailable",
|
|
||||||
systemImage: "exclamationmark.triangle")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.task {
|
|
||||||
discovery.start()
|
|
||||||
}
|
|
||||||
.onDisappear {
|
|
||||||
discovery.stop()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
|
.background(backgroundGradient.ignoresSafeArea())
|
||||||
|
.sheet(isPresented: $showingAdd) {
|
||||||
|
AddConnectionView(prefill: addPrefill)
|
||||||
|
}
|
||||||
|
.sheet(item: $editingConnection) { connection in
|
||||||
|
AddConnectionView(editing: connection)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showingSettings) {
|
||||||
|
SettingsView()
|
||||||
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.fullScreenCover(item: $sessionConnection) { connection in
|
||||||
|
SessionView(connection: connection)
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
.sheet(item: $sessionConnection) { connection in
|
||||||
|
SessionView(connection: connection)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
.task { discovery.start() }
|
||||||
|
.onDisappear { discovery.stop() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Background
|
// MARK: Background
|
||||||
@@ -81,29 +59,28 @@ public struct ConnectionListView: View {
|
|||||||
startPoint: .top,
|
startPoint: .top,
|
||||||
endPoint: .bottom
|
endPoint: .bottom
|
||||||
)
|
)
|
||||||
// soft accent bloom up top so the floating buttons feel anchored
|
|
||||||
RadialGradient(
|
RadialGradient(
|
||||||
colors: [Color.blue.opacity(0.22), .clear],
|
colors: [Color.blue.opacity(0.20), .clear],
|
||||||
center: .init(x: 0.5, y: 0.0),
|
center: .init(x: 0.5, y: 0.05),
|
||||||
startRadius: 20,
|
startRadius: 10,
|
||||||
endRadius: 320
|
endRadius: 360
|
||||||
)
|
)
|
||||||
.blendMode(.screen)
|
.blendMode(.screen)
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Top chrome (custom — replaces nav bar)
|
// MARK: Top chrome — flush with safe area top
|
||||||
|
|
||||||
private var topChrome: some View {
|
private var topChrome: some View {
|
||||||
VStack(spacing: 14) {
|
VStack(spacing: 12) {
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
circularGlassButton(systemName: "gearshape", label: "Settings") {
|
circularGlassButton(systemName: "gearshape", label: "Settings") {
|
||||||
showingSettings = true
|
showingSettings = true
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("Screens")
|
Text("Screens")
|
||||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
.font(.system(size: 26, weight: .bold, design: .rounded))
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
Spacer()
|
Spacer()
|
||||||
circularGlassButton(systemName: "plus", label: "Add connection") {
|
circularGlassButton(systemName: "plus", label: "Add connection") {
|
||||||
@@ -111,10 +88,11 @@ public struct ConnectionListView: View {
|
|||||||
showingAdd = true
|
showingAdd = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
searchField
|
searchField
|
||||||
}
|
}
|
||||||
.padding(.top, 4)
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.top, 8)
|
||||||
|
.padding(.bottom, 12)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func circularGlassButton(systemName: String,
|
private func circularGlassButton(systemName: String,
|
||||||
@@ -124,7 +102,7 @@ public struct ConnectionListView: View {
|
|||||||
Image(systemName: systemName)
|
Image(systemName: systemName)
|
||||||
.font(.callout.weight(.semibold))
|
.font(.callout.weight(.semibold))
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
.frame(width: 40, height: 40)
|
.frame(width: 38, height: 38)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.glassSurface(in: Circle())
|
.glassSurface(in: Circle())
|
||||||
@@ -160,6 +138,26 @@ public struct ConnectionListView: View {
|
|||||||
.glassSurface(in: Capsule())
|
.glassSurface(in: Capsule())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Content
|
||||||
|
|
||||||
|
private var contentList: some View {
|
||||||
|
ScrollView {
|
||||||
|
LazyVStack(spacing: 22) {
|
||||||
|
if !discovery.hosts.isEmpty || discovery.isBrowsing {
|
||||||
|
discoveredSection
|
||||||
|
}
|
||||||
|
savedSection
|
||||||
|
Color.clear.frame(height: 1) // bottom anchor
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.top, 4)
|
||||||
|
.padding(.bottom, 24)
|
||||||
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.scrollIndicators(.hidden)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Discovered
|
// MARK: Discovered
|
||||||
|
|
||||||
private var discoveredSection: some View {
|
private var discoveredSection: some View {
|
||||||
@@ -257,7 +255,7 @@ public struct ConnectionListView: View {
|
|||||||
Text("No saved connections")
|
Text("No saved connections")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
Text("Tap the + button up top to add a Mac, Linux box, or Raspberry Pi.")
|
Text("Tap + at the top to add a Mac, Linux box, or Raspberry Pi.")
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.foregroundStyle(.white.opacity(0.65))
|
.foregroundStyle(.white.opacity(0.65))
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
@@ -299,7 +297,9 @@ public struct ConnectionListView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func connectionRow(_ connection: SavedConnection) -> some View {
|
private func connectionRow(_ connection: SavedConnection) -> some View {
|
||||||
NavigationLink(value: SessionRoute(connectionID: connection.id)) {
|
Button {
|
||||||
|
sessionConnection = connection
|
||||||
|
} label: {
|
||||||
ConnectionCard(connection: connection)
|
ConnectionCard(connection: connection)
|
||||||
.padding(14)
|
.padding(14)
|
||||||
}
|
}
|
||||||
@@ -379,10 +379,6 @@ public struct ConnectionListView: View {
|
|||||||
showingAdd = true
|
showingAdd = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func connection(with id: UUID) -> SavedConnection? {
|
|
||||||
connections.first(where: { $0.id == id })
|
|
||||||
}
|
|
||||||
|
|
||||||
private func delete(_ connection: SavedConnection) {
|
private func delete(_ connection: SavedConnection) {
|
||||||
try? KeychainService().deletePassword(account: connection.keychainTag)
|
try? KeychainService().deletePassword(account: connection.keychainTag)
|
||||||
modelContext.delete(connection)
|
modelContext.delete(connection)
|
||||||
|
|||||||
Reference in New Issue
Block a user