From eaf20350a1b0009d12585b666090c97f871970a9 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 15 Oct 2026 08:36:24 -0400 Subject: [PATCH] fix(home): draw unselected tab glyphs and labels white on every iOS version iOS 27 already paints unselected tab glyphs in textMain. iOS 26 drew them in textSecondary, and the legacy pill bar dimmed whole items to 50%. Both now match iOS 27; the profile photo keeps its half-opacity unselected state. --- Flipcash/Core/Screens/Main/Home/HomeTabBar.swift | 5 +++-- Flipcash/Core/Screens/Main/Home/HomeTabView.swift | 11 ++++++----- .../Core/Screens/Main/Home/TabBarBadgedIcon.swift | 11 +---------- .../Core/Screens/Main/Home/TabBarProfilePhoto.swift | 4 ++-- 4 files changed, 12 insertions(+), 19 deletions(-) diff --git a/Flipcash/Core/Screens/Main/Home/HomeTabBar.swift b/Flipcash/Core/Screens/Main/Home/HomeTabBar.swift index 9dc1a5194..8c6508d82 100644 --- a/Flipcash/Core/Screens/Main/Home/HomeTabBar.swift +++ b/Flipcash/Core/Screens/Main/Home/HomeTabBar.swift @@ -9,7 +9,8 @@ import FlipcashUI /// The floating pill tab bar for the v2 UI. A Liquid Glass capsule holds one /// button per ``HomeTab``, with a white selection pill that slides to the active /// tab. Ported from Android's `NavigationBarV2` (Figma frame 9013-5434): -/// translucent capsule, white-20% sliding indicator, icons at full/half opacity. +/// translucent capsule, white-20% sliding indicator, white icons and labels in +/// both states. Only the profile photo dims when unselected. struct HomeTabBar: View { @Binding var selection: HomeTab @@ -80,7 +81,6 @@ struct HomeTabBar: View { .frame(height: Self.labelLineHeight) .accessibilityHidden(true) } - .opacity(selection == tab ? 1 : 0.5) .frame(width: itemWidth, height: Self.itemHeight) .contentShape(Capsule()) } @@ -110,6 +110,7 @@ struct HomeTabBar: View { private func icon(for tab: HomeTab) -> some View { if tab == .tipCard, let profileSlot { ProfileTabIcon(photo: profileSlot.preview, isSelected: tab == selection) + .opacity(tab == selection ? 1 : 0.5) .scaleEffect(Self.iconSize / ProfileTabIcon.slotSize) .frame(width: Self.iconSize, height: Self.iconSize) } else { diff --git a/Flipcash/Core/Screens/Main/Home/HomeTabView.swift b/Flipcash/Core/Screens/Main/Home/HomeTabView.swift index 7f46238f9..5b3214e91 100644 --- a/Flipcash/Core/Screens/Main/Home/HomeTabView.swift +++ b/Flipcash/Core/Screens/Main/Home/HomeTabView.swift @@ -38,18 +38,19 @@ struct HomeTabView: View { _selection = State(initialValue: initialTab) _hasOpenedChat = State(initialValue: initialTab == .chat) - // Color the tab glyphs per state: selected white, the rest secondary. - // The iOS 26 tab bar ignores UITabBar.unselectedItemTintColor, so drive - // it through a full item appearance instead. A transparent background + // Color the tab glyphs white in both states, as iOS 27 already does; the + // outline-vs-filled glyph carries the selection. The iOS 26 tab bar + // ignores UITabBar.unselectedItemTintColor, so drive it through a full + // item appearance instead. A transparent background // keeps the system's Liquid Glass. No-op on the legacy pill path, which // draws its own buttons rather than a `UITabBar`. if #available(iOS 26, *) { let items = UITabBarItemAppearance() - items.normal.iconColor = UIColor(Color.textSecondary) + items.normal.iconColor = UIColor(Color.textMain) items.selected.iconColor = UIColor(Color.textMain) // Labels per node 10642:1325: 10pt Avenir Demi, colored like the glyph above. let titleFont = UIFont.default(size: 10, weight: .semibold) - items.normal.titleTextAttributes = [.font: titleFont, .foregroundColor: UIColor(Color.textSecondary)] + items.normal.titleTextAttributes = [.font: titleFont, .foregroundColor: UIColor(Color.textMain)] items.selected.titleTextAttributes = [.font: titleFont, .foregroundColor: UIColor(Color.textMain)] let appearance = UITabBarAppearance() diff --git a/Flipcash/Core/Screens/Main/Home/TabBarBadgedIcon.swift b/Flipcash/Core/Screens/Main/Home/TabBarBadgedIcon.swift index 168862e85..427e7bd2e 100644 --- a/Flipcash/Core/Screens/Main/Home/TabBarBadgedIcon.swift +++ b/Flipcash/Core/Screens/Main/Home/TabBarBadgedIcon.swift @@ -48,7 +48,7 @@ enum TabBarBadgedIcon { .renderingMode(.template) .resizable() .scaledToFit() - .foregroundStyle(glyphColor(isSelected: isSelected)) + .foregroundStyle(Color.textMain) .frame(width: glyphSize, height: glyphSize) .overlay(alignment: .topTrailing) { Bubble(size: .regular, count: count, color: .unreadIndicator) @@ -59,15 +59,6 @@ enum TabBarBadgedIcon { .padding(.trailing, bubbleOverhang) } - /// The color the bar paints its template glyphs in for the state. iOS 27 - /// ignores the unselected `iconColor` from `HomeTabView` and paints every - /// glyph in the main text color, so the Chat glyph follows suit there. - private static func glyphColor(isSelected: Bool) -> Color { - if isSelected { return .textMain } - if #available(iOS 27, *) { return .textMain } - return .textSecondary - } - private static func image(of icon: some View) -> UIImage? { let renderer = ImageRenderer(content: icon) renderer.scale = UIScreen.main.scale diff --git a/Flipcash/Core/Screens/Main/Home/TabBarProfilePhoto.swift b/Flipcash/Core/Screens/Main/Home/TabBarProfilePhoto.swift index 42b5abc59..03fc4907a 100644 --- a/Flipcash/Core/Screens/Main/Home/TabBarProfilePhoto.swift +++ b/Flipcash/Core/Screens/Main/Home/TabBarProfilePhoto.swift @@ -160,8 +160,8 @@ final class TabBarProfilePhoto { /// /// The bar tints template glyphs per state, but a photo has to render as its /// own colours — so the unselected state is baked in here instead: the - /// thinner half-opacity ring, and the half opacity the pill applies to every - /// other icon. + /// thinner half-opacity ring, and the half opacity the pill draws the photo + /// at. static func render(_ photo: UIImage?) -> ItemImages? { guard let selected = image(of: ProfileTabIcon(photo: photo, isSelected: true)), let normal = image(of: ProfileTabIcon(photo: photo, isSelected: false).opacity(0.5))