From f72f4297e151803b5dc9fa2d1e80c9562905fccf Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Sun, 5 Jul 2026 18:31:25 -0700 Subject: [PATCH] Tab Bar Load Animation Nucleic-Session: EEC60FAE-8368-49CA-9202-7026C12E7F8C Co-authored-by: Nucleic --- .../Views/AdaptiveRootView.swift | 55 ++++++++++++++----- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/NucleicRemote/NucleicRemote/Views/AdaptiveRootView.swift b/NucleicRemote/NucleicRemote/Views/AdaptiveRootView.swift index ef4c872..9defdfe 100644 --- a/NucleicRemote/NucleicRemote/Views/AdaptiveRootView.swift +++ b/NucleicRemote/NucleicRemote/Views/AdaptiveRootView.swift @@ -74,10 +74,13 @@ struct CompactRootView: View { } } -/// Our own bottom tab bar for the compact shell — a plain `.bar`-material strip of five items, -/// standing in for the system tab bar so `CompactRootView` can animate its visibility itself -/// (see that view for why). The material bleeds into the home-indicator area; the items stay in -/// the safe region. The Sessions item carries the "needs you" badge the system tab item used to. +/// Our own bottom tab bar for the compact shell — a floating Liquid Glass capsule of five items, +/// standing in for the system tab bar so `CompactRootView` can animate its visibility itself (see +/// that view for why) while keeping the iOS 26 floating-glass look. Real `glassEffect` on iOS 26; +/// an ultra-thin material capsule with a hairline edge + soft shadow on earlier systems, matching +/// the app's `glassSurface`/`glassCircle`. It floats inset from the edges, above the home +/// indicator — it does not bleed to the screen edge. The Sessions item carries the "needs you" +/// badge the system tab item used to. struct NucleicTabBar: View { @Binding var selection: Int /// Sessions blocking on a human — the red badge on the Sessions item (was the tab item badge). @@ -98,15 +101,13 @@ struct NucleicTabBar: View { itemButton(item) } } - .padding(.top, 8) - .padding(.bottom, 2) - // A hairline top separator, like the system bar's edge over scrolling content. - .overlay(alignment: .top) { - Rectangle().fill(Color.primary.opacity(0.10)).frame(height: 0.5) - } - // `.bar` is the material the system uses for tab/tool bars; bleed it into the bottom - // unsafe area so it reaches the screen edge under the home indicator. - .background(.bar, ignoresSafeAreaEdges: .bottom) + .padding(.horizontal, 6) + .padding(.vertical, 8) + .glassBar() + // Float the capsule inset from the edges and just above the home indicator, rather than + // filling the bottom edge like the old opaque bar. + .padding(.horizontal, 16) + .padding(.bottom, 4) } private func itemButton(_ item: Item) -> some View { @@ -116,8 +117,8 @@ struct NucleicTabBar: View { } label: { VStack(spacing: 3) { Image(systemName: item.icon) - .font(.system(size: 22, weight: selected ? .semibold : .regular)) - .frame(height: 26) + .font(.system(size: 21, weight: selected ? .semibold : .regular)) + .frame(height: 24) .overlay(alignment: .topTrailing) { if item.index == 1, needsYou > 0 { Text(needsYou > 99 ? "99+" : "\(needsYou)") @@ -133,6 +134,14 @@ struct NucleicTabBar: View { } .foregroundStyle(selected ? Palette.accent : Color.secondary) .frame(maxWidth: .infinity) + .padding(.vertical, 4) + // The selected item rides a soft accent-tinted capsule, echoing the system floating + // bar's highlighted tab. + .background { + if selected { + Capsule().fill(Palette.accent.opacity(0.15)) + } + } .contentShape(Rectangle()) } .buttonStyle(.plain) @@ -141,6 +150,22 @@ struct NucleicTabBar: View { } } +private extension View { + /// The floating tab bar's Liquid Glass capsule backing — the capsule twin of `glassSurface`. + /// Real `glassEffect` on iOS 26; an ultra-thin material capsule with a hairline edge and soft + /// shadow on earlier systems so it still lifts off the content. + @ViewBuilder + func glassBar() -> some View { + if #available(iOS 26.0, *) { + glassEffect(.regular, in: .capsule) + } else { + background(.ultraThinMaterial, in: Capsule()) + .overlay(Capsule().strokeBorder(Color.primary.opacity(0.08), lineWidth: 1)) + .shadow(color: .black.opacity(0.18), radius: 10, y: 3) + } + } +} + // MARK: - Regular (iPad sidebar + detail) /// What the sidebar can select. Mirrors the Mac sidebar: a few global destinations plus the