From 8596a699c15ada4331feee629e674729666c770b Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 9 Jan 2026 15:50:39 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- BusinessCard/Design/DesignConstants.swift | 63 +++++++++++++++++++++-- BusinessCard/Views/CardEditorView.swift | 2 + BusinessCard/Views/CardsHomeView.swift | 2 + BusinessCard/Views/RootTabView.swift | 40 +++++++------- 4 files changed, 84 insertions(+), 23 deletions(-) diff --git a/BusinessCard/Design/DesignConstants.swift b/BusinessCard/Design/DesignConstants.swift index ea583df..81b79ff 100644 --- a/BusinessCard/Design/DesignConstants.swift +++ b/BusinessCard/Design/DesignConstants.swift @@ -8,10 +8,53 @@ import SwiftUI import Bedrock +// MARK: - Device Size Tier + +/// Categorizes devices by screen size for adaptive layouts. +/// Primarily used to adjust card dimensions on smaller iPhones and constrain width on iPad. +enum DeviceSizeTier { + case compact // iPhone SE, Mini, older 4.7" devices (height < 700pt) + case standard // Regular iPhone (height 700-850pt) + case large // Pro Max, Plus models (height > 850pt) + case tablet // iPad + + /// The current device's size tier based on screen dimensions. + static var current: DeviceSizeTier { + let bounds = UIScreen.main.bounds + let height = max(bounds.width, bounds.height) + let width = min(bounds.width, bounds.height) + + // iPad detection: width >= 768pt in portrait + if width >= 768 { + return .tablet + } + + switch height { + case ..<700: + return .compact + case ..<850: + return .standard + default: + return .large + } + } + + /// Whether the current device is an iPad. + static var isTablet: Bool { + current == .tablet + } + + /// Whether the current device is a compact iPhone (SE, Mini). + static var isCompact: Bool { + current == .compact + } +} + // MARK: - App-Specific Sizes extension Design { /// BusinessCard-specific size constants. + /// Some values adapt based on device size tier. enum CardSize { static let cardWidth: CGFloat = 320 static let cardHeight: CGFloat = 340 @@ -20,7 +63,17 @@ extension Design { static let avatarLarge: CGFloat = 80 static let avatarOverlap: CGFloat = 40 static let logoSize: CGFloat = 64 - static let bannerHeight: CGFloat = 240 + + /// Banner height adapts to device size to prevent dominating small screens. + static var bannerHeight: CGFloat { + switch DeviceSizeTier.current { + case .compact: return 180 + case .standard: return 210 + case .large: return 240 + case .tablet: return 240 + } + } + static let qrSize: CGFloat = 200 static let qrSizeLarge: CGFloat = 260 static let colorSwatchSize: CGFloat = 40 @@ -29,10 +82,14 @@ extension Design { static let widgetPhoneHeight: CGFloat = 120 static let widgetWatchSize: CGFloat = 100 static let floatingButtonSize: CGFloat = 56 - /// Bottom offset for floating button above tab bar. - static let floatingButtonBottomOffset: CGFloat = 72 /// Aspect ratio for logo container (3:2 landscape). static let logoContainerAspectRatio: CGFloat = 3.0 / 2.0 + + /// Maximum card width to prevent stretching on iPad. + /// On iPhone this returns nil (no constraint needed). + static var maxCardWidth: CGFloat? { + DeviceSizeTier.isTablet ? 500 : nil + } } } diff --git a/BusinessCard/Views/CardEditorView.swift b/BusinessCard/Views/CardEditorView.swift index 7c91603..50a840d 100644 --- a/BusinessCard/Views/CardEditorView.swift +++ b/BusinessCard/Views/CardEditorView.swift @@ -1082,6 +1082,8 @@ private struct CardPreviewSheet: View { NavigationStack { ScrollView { BusinessCardView(card: card) + .frame(maxWidth: Design.CardSize.maxCardWidth) + .frame(maxWidth: .infinity) .padding(Design.Spacing.large) } .background(Color.AppBackground.base) diff --git a/BusinessCard/Views/CardsHomeView.swift b/BusinessCard/Views/CardsHomeView.swift index d5dee4f..f7ce4c1 100644 --- a/BusinessCard/Views/CardsHomeView.swift +++ b/BusinessCard/Views/CardsHomeView.swift @@ -98,7 +98,9 @@ private struct CardPageView: View { ScrollView { VStack(spacing: Design.Spacing.large) { BusinessCardView(card: card) + .frame(maxWidth: Design.CardSize.maxCardWidth) } + .frame(maxWidth: .infinity) .padding(.horizontal, Design.Spacing.large) .padding(.vertical, Design.Spacing.xLarge) } diff --git a/BusinessCard/Views/RootTabView.swift b/BusinessCard/Views/RootTabView.swift index f5bc3ab..930c123 100644 --- a/BusinessCard/Views/RootTabView.swift +++ b/BusinessCard/Views/RootTabView.swift @@ -9,26 +9,27 @@ struct RootTabView: View { var body: some View { @Bindable var appState = appState - ZStack(alignment: .bottom) { - TabView(selection: $appState.selectedTab) { - Tab(String.localized("My Cards"), systemImage: "rectangle.stack", value: AppTab.cards) { - CardsHomeView() - } - - Tab(String.localized("Contacts"), systemImage: "person.2", value: AppTab.contacts) { - ContactsView() - } - - Tab(String.localized("Widgets"), systemImage: "square.grid.2x2", value: AppTab.widgets) { - WidgetsView() - } + TabView(selection: $appState.selectedTab) { + Tab(String.localized("My Cards"), systemImage: "rectangle.stack", value: AppTab.cards) { + CardsHomeView() + .safeAreaInset(edge: .bottom, spacing: 0) { + // Floating share button positioned above tab bar + if !appState.cardStore.cards.isEmpty { + FloatingShareButton { + showingShareSheet = true + } + .frame(maxWidth: .infinity) + .padding(.bottom, Design.Spacing.small) + } + } } - - // Floating share button - only shown on cards tab when cards exist - if appState.selectedTab == .cards && !appState.cardStore.cards.isEmpty { - FloatingShareButton { - showingShareSheet = true - } + + Tab(String.localized("Contacts"), systemImage: "person.2", value: AppTab.contacts) { + ContactsView() + } + + Tab(String.localized("Widgets"), systemImage: "square.grid.2x2", value: AppTab.widgets) { + WidgetsView() } } .sheet(isPresented: $showingShareSheet) { @@ -62,7 +63,6 @@ private struct FloatingShareButton: View { } .accessibilityLabel(String.localized("Share")) .accessibilityHint(String.localized("Opens the share sheet to send your card")) - .padding(.bottom, Design.CardSize.floatingButtonBottomOffset) } }