From edd9218738d862a59eb423eaa5ade45531e36b47 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 22 Dec 2025 22:00:46 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- Baccarat/Baccarat.xcodeproj/project.pbxproj | 2 + Baccarat/Baccarat/Theme/DesignConstants.swift | 45 ++-- .../Views/Table/CardsDisplayArea.swift | 22 +- .../Views/Table/CompactHandView.swift | 18 +- .../Baccarat/Views/Table/HandValueBadge.swift | 2 +- Blackjack/Blackjack.xcodeproj/project.pbxproj | 2 + .../Blackjack/Theme/DesignConstants.swift | 18 +- CasinoKit/Package.swift | 6 + .../CasinoKit/Theme/CasinoDesign.swift | 21 ++ .../CasinoKit/Utilities/DeviceInfo.swift | 196 ++++++++++++++++++ 10 files changed, 274 insertions(+), 58 deletions(-) create mode 100644 CasinoKit/Sources/CasinoKit/Utilities/DeviceInfo.swift diff --git a/Baccarat/Baccarat.xcodeproj/project.pbxproj b/Baccarat/Baccarat.xcodeproj/project.pbxproj index 748b2a6..1e35843 100644 --- a/Baccarat/Baccarat.xcodeproj/project.pbxproj +++ b/Baccarat/Baccarat.xcodeproj/project.pbxproj @@ -433,6 +433,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -470,6 +471,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Baccarat/Baccarat/Theme/DesignConstants.swift b/Baccarat/Baccarat/Theme/DesignConstants.swift index 9999008..15c3e47 100644 --- a/Baccarat/Baccarat/Theme/DesignConstants.swift +++ b/Baccarat/Baccarat/Theme/DesignConstants.swift @@ -34,28 +34,13 @@ enum Design { enum Size { // MARK: - Hand Scaling - /// Hand scaling factor for small screens (iPhone SE, etc). - /// Applied on screens narrower than smallScreenThreshold. - static let handScaleSmall: CGFloat = 1.75 - - /// Hand scaling factor for standard+ iPhones. - /// Applied on screens wider than smallScreenThreshold. - static let handScaleLarge: CGFloat = 2.0 - - /// Screen width threshold for small screen detection (iPhone SE is 375pt) - static let smallScreenThreshold: CGFloat = 390 + // Use shared scaling from CasinoKit + static var handScale: CGFloat { CasinoDesign.Size.handScale } + static var fontScale: CGFloat { CasinoDesign.Size.fontScale } /// Additional scale multiplier for large screens (iPad). - /// Applied on top of handScale when on regular size class. static let largeScreenMultiplier: CGFloat = 1.2 - /// Returns the appropriate hand scale based on screen width. - /// - Parameter screenWidth: The current screen width in points - /// - Returns: handScaleSmall for iPhone SE, handScaleLarge for larger phones - static func handScale(for screenWidth: CGFloat) -> CGFloat { - screenWidth <= smallScreenThreshold ? handScaleSmall : handScaleLarge - } - // Cards - base values from CasinoDesign static let cardWidthSmall: CGFloat = CasinoDesign.Size.cardWidthSmall static let cardWidthMedium: CGFloat = CasinoDesign.Size.cardWidthMedium @@ -69,24 +54,24 @@ enum Design { /// More negative = more overlap (less card visible). private static let cardOverlapBase: CGFloat = -25 - /// Returns card width for the given screen width - static func cardWidthTable(for screenWidth: CGFloat) -> CGFloat { - cardWidthTableBase * handScale(for: screenWidth) + /// Card width scaled for the current device. + static var cardWidthTable: CGFloat { + cardWidthTableBase * handScale } - /// Returns card width for iPad (with large screen multiplier) - static func cardWidthTableLarge(for screenWidth: CGFloat) -> CGFloat { - cardWidthTableBase * handScale(for: screenWidth) * largeScreenMultiplier + /// Card width scaled for iPad (with large screen multiplier). + static var cardWidthTableLarge: CGFloat { + cardWidthTableBase * handScale * largeScreenMultiplier } - /// Returns card overlap for the given screen width - static func cardOverlap(for screenWidth: CGFloat) -> CGFloat { - cardOverlapBase * handScale(for: screenWidth) + /// Card overlap scaled for the current device. + static var cardOverlap: CGFloat { + cardOverlapBase * handScale } - /// Returns card overlap for iPad (with large screen multiplier) - static func cardOverlapLarge(for screenWidth: CGFloat) -> CGFloat { - cardOverlapBase * handScale(for: screenWidth) * largeScreenMultiplier + /// Card overlap scaled for iPad (with large screen multiplier). + static var cardOverlapLarge: CGFloat { + cardOverlapBase * handScale * largeScreenMultiplier } // Chips - use CasinoDesign values diff --git a/Baccarat/Baccarat/Views/Table/CardsDisplayArea.swift b/Baccarat/Baccarat/Views/Table/CardsDisplayArea.swift index e0d81d6..73d4974 100644 --- a/Baccarat/Baccarat/Views/Table/CardsDisplayArea.swift +++ b/Baccarat/Baccarat/Views/Table/CardsDisplayArea.swift @@ -36,16 +36,26 @@ struct CardsDisplayArea: View { // Use global debug flag from Design constants private var showDebugBorders: Bool { Design.showDebugBorders } - /// Label font size - only scales on iPad to avoid clipping on small iPhones + /// Label font size - scales differently for small devices, standard iPhones, and iPads private var labelFontSize: CGFloat { - let baseSize: CGFloat = 14 - return isLargeScreen ? baseSize * Design.Size.handScale(for: screenWidth) * Design.Size.largeScreenMultiplier : baseSize + if isLargeScreen { + return 14 * Design.Size.handScale * Design.Size.largeScreenMultiplier + } else if DeviceInfo.isSmallDevice { + return 12 // Smaller font for iPhone SE + } else { + return 14 // Standard iPhone + } } - /// Minimum height for label row - only scales on iPad + /// Minimum height for label row - scales differently for small devices, standard iPhones, and iPads private var labelRowMinHeight: CGFloat { - let baseHeight: CGFloat = 30 - return isLargeScreen ? baseHeight * Design.Size.handScale(for: screenWidth) * Design.Size.largeScreenMultiplier : baseHeight + if isLargeScreen { + return 30 * Design.Size.handScale * Design.Size.largeScreenMultiplier + } else if DeviceInfo.isSmallDevice { + return 24 // Smaller row for iPhone SE + } else { + return 30 // Standard iPhone + } } /// Spacing between PLAYER and BANKER hands - reduced on smaller screens diff --git a/Baccarat/Baccarat/Views/Table/CompactHandView.swift b/Baccarat/Baccarat/Views/Table/CompactHandView.swift index 6c7f81e..117ae63 100644 --- a/Baccarat/Baccarat/Views/Table/CompactHandView.swift +++ b/Baccarat/Baccarat/Views/Table/CompactHandView.swift @@ -30,16 +30,12 @@ struct CompactHandView: View { /// WIN badge font size - only scales on iPad private var winBadgeFontSize: CGFloat { let baseSize: CGFloat = 10 - return isLargeScreen ? baseSize * Design.Size.handScale(for: screenWidth) * Design.Size.largeScreenMultiplier : baseSize + return isLargeScreen ? baseSize * Design.Size.handScale * Design.Size.largeScreenMultiplier : baseSize } - /// Card width - responsive based on screen size + /// Card width - responsive based on device type (uses DeviceKit) private var cardWidth: CGFloat { - if isLargeScreen { - return Design.Size.cardWidthTableLarge(for: screenWidth) - } else { - return Design.Size.cardWidthTable(for: screenWidth) - } + isLargeScreen ? Design.Size.cardWidthTableLarge : Design.Size.cardWidthTable } /// Card height based on aspect ratio @@ -47,13 +43,9 @@ struct CompactHandView: View { cardWidth * Design.Size.cardAspectRatio } - /// Card overlap - scaled with card size + /// Card overlap - scaled with card size (uses DeviceKit) private var cardOverlap: CGFloat { - if isLargeScreen { - return Design.Size.cardOverlapLarge(for: screenWidth) - } else { - return Design.Size.cardOverlap(for: screenWidth) - } + isLargeScreen ? Design.Size.cardOverlapLarge : Design.Size.cardOverlap } private let placeholderSpacing: CGFloat = Design.Spacing.small diff --git a/Baccarat/Baccarat/Views/Table/HandValueBadge.swift b/Baccarat/Baccarat/Views/Table/HandValueBadge.swift index 4137b7b..54217fc 100644 --- a/Baccarat/Baccarat/Views/Table/HandValueBadge.swift +++ b/Baccarat/Baccarat/Views/Table/HandValueBadge.swift @@ -26,7 +26,7 @@ struct HandValueBadge: View { /// Scale factor for badge sizing - only applies on iPad to avoid clipping on iPhone private var scale: CGFloat { - isLargeScreen ? Design.Size.handScaleLarge * Design.Size.largeScreenMultiplier : 1.0 + isLargeScreen ? Design.Size.handScale * Design.Size.largeScreenMultiplier : 1.0 } @ScaledMetric(relativeTo: .headline) private var baseValueFontSize: CGFloat = 15 diff --git a/Blackjack/Blackjack.xcodeproj/project.pbxproj b/Blackjack/Blackjack.xcodeproj/project.pbxproj index 23f4c2a..74fec65 100644 --- a/Blackjack/Blackjack.xcodeproj/project.pbxproj +++ b/Blackjack/Blackjack.xcodeproj/project.pbxproj @@ -416,6 +416,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -448,6 +449,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Blackjack/Blackjack/Theme/DesignConstants.swift b/Blackjack/Blackjack/Theme/DesignConstants.swift index a42047b..d59fb50 100644 --- a/Blackjack/Blackjack/Theme/DesignConstants.swift +++ b/Blackjack/Blackjack/Theme/DesignConstants.swift @@ -35,9 +35,11 @@ enum Design { // MARK: - Blackjack-Specific Component Sizes enum Size { - // Hand scaling factor (1.5 = 50% larger hands) - static let handScale: CGFloat = 1.75 + // Use shared scaling from CasinoKit + static var handScale: CGFloat { CasinoDesign.Size.handScale } + static var fontScale: CGFloat { CasinoDesign.Size.fontScale } + // Cards - scaled for better visibility static let cardWidth: CGFloat = 60 * handScale // 90pt at 1.5x static let cardWidthSmall: CGFloat = CasinoDesign.Size.cardWidthSmall @@ -52,12 +54,12 @@ enum Design { static let playerHandsHeight: CGFloat = 160 * handScale // 240pt at 1.5x // Hand label font sizes (scaled) - static let handLabelFontSize: CGFloat = CasinoDesign.BaseFontSize.medium * handScale - static let handNumberFontSize: CGFloat = CasinoDesign.BaseFontSize.medium * handScale // Same as label - static let handValueFontSize: CGFloat = CasinoDesign.BaseFontSize.xLarge * handScale + static let handLabelFontSize: CGFloat = CasinoDesign.BaseFontSize.medium * fontScale + static let handNumberFontSize: CGFloat = CasinoDesign.BaseFontSize.medium * fontScale // Same as label + static let handValueFontSize: CGFloat = CasinoDesign.BaseFontSize.xLarge * fontScale // Hint font size (scaled to match hands) - static let hintFontSize: CGFloat = CasinoDesign.BaseFontSize.small * handScale + static let hintFontSize: CGFloat = CasinoDesign.BaseFontSize.small * fontScale static let hintIconSize: CGFloat = CasinoDesign.IconSize.medium * handScale static let hintPaddingH: CGFloat = CasinoDesign.Spacing.medium * handScale static let hintPaddingV: CGFloat = CasinoDesign.Spacing.small * handScale @@ -66,7 +68,7 @@ enum Design { static let handIconSize: CGFloat = CasinoDesign.IconSize.medium * handScale // Hi-Lo count badge (scaled) - static let countBadgeFontSize: CGFloat = CasinoDesign.BaseFontSize.xxSmall * handScale + static let countBadgeFontSize: CGFloat = CasinoDesign.BaseFontSize.xxSmall * fontScale static let countBadgePaddingH: CGFloat = CasinoDesign.Spacing.xSmall * handScale static let countBadgePaddingV: CGFloat = CasinoDesign.Spacing.xxxSmall * handScale static let countBadgeOffset: CGFloat = CasinoDesign.Spacing.xSmall * handScale @@ -76,7 +78,7 @@ enum Design { static let bettingZoneHeightScaled: CGFloat = CasinoDesign.Size.bettingZoneHeight // Keep original height to save space // Card count display (scaled) - static let cardCountLabelSize: CGFloat = CasinoDesign.BaseFontSize.xSmall * handScale + static let cardCountLabelSize: CGFloat = CasinoDesign.BaseFontSize.xSmall * fontScale static let cardCountValueSize: CGFloat = CasinoDesign.BaseFontSize.large * handScale // Chips - use CasinoDesign values diff --git a/CasinoKit/Package.swift b/CasinoKit/Package.swift index 06123b4..198bb36 100644 --- a/CasinoKit/Package.swift +++ b/CasinoKit/Package.swift @@ -16,9 +16,15 @@ let package = Package( targets: ["CasinoKit"] ) ], + dependencies: [ + .package(url: "https://github.com/devicekit/DeviceKit.git", from: "5.0.0") + ], targets: [ .target( name: "CasinoKit", + dependencies: [ + .product(name: "DeviceKit", package: "DeviceKit") + ], resources: [ .process("Resources") ] diff --git a/CasinoKit/Sources/CasinoKit/Theme/CasinoDesign.swift b/CasinoKit/Sources/CasinoKit/Theme/CasinoDesign.swift index b059e52..b70457d 100644 --- a/CasinoKit/Sources/CasinoKit/Theme/CasinoDesign.swift +++ b/CasinoKit/Sources/CasinoKit/Theme/CasinoDesign.swift @@ -99,6 +99,27 @@ public enum CasinoDesign { // MARK: - Sizes public enum Size { + // MARK: - Dynamic Scaling (uses DeviceKit) + + /// Hand scaling factor for small devices (iPhone SE, iPad mini). + public static let handScaleSmall: CGFloat = 1.5 + + /// Hand scaling factor for standard+ devices. + public static let handScaleLarge: CGFloat = 2.0 + + /// Dynamic hand scale based on device type. + public static var handScale: CGFloat { + DeviceInfo.isSmallDevice ? handScaleSmall : handScaleLarge + } + + /// Dynamic font scale based on device type. + /// Slightly smaller on small devices for better fit. + public static var fontScale: CGFloat { + DeviceInfo.isSmallDevice ? handScale * 0.90 : handScale + } + + // MARK: - Chip Sizes + /// Default chip size for selectors. public static let chipSmall: CGFloat = 36 public static let chipMedium: CGFloat = 50 diff --git a/CasinoKit/Sources/CasinoKit/Utilities/DeviceInfo.swift b/CasinoKit/Sources/CasinoKit/Utilities/DeviceInfo.swift new file mode 100644 index 0000000..baa0e8f --- /dev/null +++ b/CasinoKit/Sources/CasinoKit/Utilities/DeviceInfo.swift @@ -0,0 +1,196 @@ +// +// DeviceInfo.swift +// CasinoKit +// +// Device detection utilities using DeviceKit. +// + +import SwiftUI +@_exported import DeviceKit + +/// Device information utilities for responsive layouts. +public enum DeviceInfo { + + // MARK: - Current Device + + /// The current device. + public static var current: Device { + Device.current + } + + // MARK: - Device Size Categories + public static var isSmallDevice: Bool { + isSmallPhone || isPadMini + } + + /// Whether the current device is a small iPhone (SE series). + /// Includes iPhone SE (1st, 2nd, 3rd gen) and their simulators. + public static var isSmallPhone: Bool { + let smallPhones: [Device] = [ + // iPhone SE series + .iPhoneSE, + .iPhoneSE2, + .iPhoneSE3, + // Simulators + .simulator(.iPhoneSE), + .simulator(.iPhoneSE2), + .simulator(.iPhoneSE3) + ] + return current.isOneOf(smallPhones) + } + + /// Whether the current device is a standard iPhone (not SE, not Pro Max/Plus). + public static var isStandardPhone: Bool { + current.isPhone && !isSmallPhone && !isLargePhone + } + + /// Whether the current device is a large iPhone (Pro Max, Plus models). + public static var isLargePhone: Bool { + let largePhones: [Device] = [ + // Plus models + .iPhone6Plus, .iPhone6sPlus, .iPhone7Plus, .iPhone8Plus, + // Max models + .iPhoneXSMax, .iPhone11ProMax, .iPhone12ProMax, + .iPhone13ProMax, .iPhone14Plus, .iPhone14ProMax, + .iPhone15Plus, .iPhone15ProMax, .iPhone16Plus, .iPhone16ProMax, + // Simulators + .simulator(.iPhone6Plus), .simulator(.iPhone6sPlus), + .simulator(.iPhone7Plus), .simulator(.iPhone8Plus), + .simulator(.iPhoneXSMax), .simulator(.iPhone11ProMax), + .simulator(.iPhone12ProMax), .simulator(.iPhone13ProMax), + .simulator(.iPhone14Plus), .simulator(.iPhone14ProMax), + .simulator(.iPhone15Plus), .simulator(.iPhone15ProMax), + .simulator(.iPhone16Plus), .simulator(.iPhone16ProMax) + ] + return current.isOneOf(largePhones) + } + + /// Whether the current device is an iPhone. + public static var isPhone: Bool { + current.isPhone + } + + /// Whether the current device is an iPad. + public static var isPad: Bool { + current.isPad + } + + /// Whether the current device is an iPad mini. + /// iPad minis have smaller screens than standard iPads (7.9" or 8.3"). + /// Uses screen diagonal as fallback for reliable detection on real devices. + public static var isPadMini: Bool { + // First try exact device match + let minis: [Device] = [ + .iPadMini, .iPadMini2, .iPadMini3, .iPadMini4, + .iPadMini5, .iPadMini6, .iPadMiniA17Pro, + .simulator(.iPadMini), .simulator(.iPadMini2), .simulator(.iPadMini3), + .simulator(.iPadMini4), .simulator(.iPadMini5), .simulator(.iPadMini6), + .simulator(.iPadMiniA17Pro) + ] + if current.isOneOf(minis) { + return true + } + + // Fallback: Check screen diagonal (iPad minis are 7.9" or 8.3") + // Other iPads are 10.2" and larger + if current.isPad { + let diagonal = current.diagonal + return diagonal > 0 && diagonal < 9.0 + } + + return false + } + + /// Whether the current device is a large iPad (Pro 12.9", 13"). + public static var isLargePad: Bool { + let largePads: [Device] = [ + .iPadPro12Inch, .iPadPro12Inch2, .iPadPro12Inch3, + .iPadPro12Inch4, .iPadPro12Inch5, .iPadPro12Inch6, + .iPadPro13M4, + .simulator(.iPadPro12Inch), .simulator(.iPadPro12Inch2), + .simulator(.iPadPro12Inch3), .simulator(.iPadPro12Inch4), + .simulator(.iPadPro12Inch5), .simulator(.iPadPro12Inch6), + .simulator(.iPadPro13M4) + ] + return current.isOneOf(largePads) + } + + /// Whether running in a simulator. + public static var isSimulator: Bool { + current.isSimulator + } + + // MARK: - Screen Size Helpers + + /// The diagonal screen size in inches. + public static var screenDiagonal: Double { + current.diagonal + } + + /// The screen's pixels per inch. + public static var screenPPI: Int { + current.ppi ?? 0 + } + + // MARK: - Device Name + + /// A human-readable description of the current device. + public static var deviceName: String { + current.description + } + + /// The real device when running in simulator, or the device itself. + public static var realDevice: Device { + current.realDevice + } + + /// Debug info about the current device (for troubleshooting). + public static var debugInfo: String { + let diag = String(format: "%.1f", screenDiagonal) + return "Device: \(current), diagonal: \(diag)\", isPadMini: \(isPadMini)" + } +} + +// MARK: - SwiftUI Environment + +/// Environment key for small phone detection. +private struct IsSmallPhoneKey: EnvironmentKey { + static let defaultValue: Bool = DeviceInfo.isSmallPhone +} + +extension EnvironmentValues { + /// Whether the current device is a small phone (iPhone SE series). + public var isSmallPhone: Bool { + get { self[IsSmallPhoneKey.self] } + set { self[IsSmallPhoneKey.self] = newValue } + } +} + +// MARK: - View Extension + +extension View { + /// Applies different modifiers based on device size. + /// - Parameters: + /// - small: Modifier to apply on small phones (iPhone SE) + /// - standard: Modifier to apply on standard phones + /// - large: Modifier to apply on large phones (Pro Max, Plus) + /// - pad: Modifier to apply on iPads + @ViewBuilder + public func deviceAdaptive( + small: (Self) -> Small, + standard: (Self) -> Standard, + large: (Self) -> Large, + pad: (Self) -> Pad + ) -> some View { + if DeviceInfo.isPad { + pad(self) + } else if DeviceInfo.isSmallPhone { + small(self) + } else if DeviceInfo.isLargePhone { + large(self) + } else { + standard(self) + } + } +} +