Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-10 17:47:05 -06:00
parent 44724914b4
commit 6b10f8b398
15 changed files with 514 additions and 14 deletions

View File

@ -629,6 +629,7 @@
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
"INFOPLIST_KEY_UILaunchScreen_BackgroundColor" = LaunchBackground;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -665,6 +666,7 @@
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
"INFOPLIST_KEY_UILaunchScreen_BackgroundColor" = LaunchBackground;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -851,6 +853,7 @@
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
"INFOPLIST_KEY_UILaunchScreen_BackgroundColor" = LaunchBackground;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LD_RUNPATH_SEARCH_PATHS = (
@ -888,6 +891,7 @@
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
"INFOPLIST_KEY_UILaunchScreen_BackgroundColor" = LaunchBackground;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LD_RUNPATH_SEARCH_PATHS = (

View File

@ -7,12 +7,12 @@
<key>BusinessCard.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
<integer>1</integer>
</dict>
<key>BusinessCardClip.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
<integer>2</integer>
</dict>
<key>BusinessCardWatch Watch App.xcscheme_^#shared#^_</key>
<dict>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -1,6 +1,7 @@
{
"images" : [
{
"filename" : "AppIcon.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.278",
"green" : "0.329",
"red" : "0.949"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.278",
"green" : "0.329",
"red" : "0.949"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -72,9 +72,17 @@ struct BusinessCardApp: App {
var body: some Scene {
WindowGroup {
RootTabView()
.environment(appState)
.preferredColorScheme(.light)
ZStack {
// Base background matching launch screen - prevents white flash
Color.Branding.primary
.ignoresSafeArea()
AppLaunchView(config: .businessCard) {
RootTabView()
.environment(appState)
.preferredColorScheme(.light)
}
}
}
.modelContainer(modelContainer)
}

View File

@ -0,0 +1,63 @@
//
// BrandingConfig.swift
// BusinessCard
//
// App-specific branding configurations for icons and launch screens.
//
import SwiftUI
import Bedrock
// MARK: - App Branding Colors
extension Color {
/// BusinessCard branding colors for icon and launch screen.
enum Branding {
/// Primary gradient color (warm coral/red).
/// Must match LaunchBackground.colorset exactly to prevent flash.
static let primary = Color(red: 0.949, green: 0.329, blue: 0.278)
/// Secondary gradient color (darker red).
static let secondary = Color(red: 0.65, green: 0.18, blue: 0.15)
/// Accent color for icons and highlights.
static let accent = Color.white
}
}
// MARK: - App Icon Configuration
extension AppIconConfig {
/// BusinessCard app icon configuration.
static let businessCard = AppIconConfig(
title: "CARDS",
subtitle: nil,
iconSymbol: "person.text.rectangle",
primaryColor: Color.Branding.primary,
secondaryColor: Color.Branding.secondary,
accentColor: Color.Branding.accent
)
}
// MARK: - Launch Screen Configuration
extension LaunchScreenConfig {
/// BusinessCard launch screen configuration.
static let businessCard = LaunchScreenConfig(
title: "BUSINESS CARD",
tagline: "Share Your Professional Identity",
iconSymbols: ["person.text.rectangle"],
cornerSymbol: nil,
decorativeSymbol: "circle.fill",
patternStyle: .dots,
layoutStyle: .iconAboveTitle,
primaryColor: Color.Branding.primary,
secondaryColor: Color.Branding.secondary,
accentColor: Color.Branding.accent,
titleColor: .white,
iconSize: 52,
titleSize: 32,
iconSpacing: 12,
animationDuration: 0.6
)
}

View File

@ -0,0 +1,136 @@
//
// BusinessCardTheme.swift
// BusinessCard
//
// App-specific theme conforming to Bedrock's color protocols.
// This light theme uses warm, professional tones.
//
import SwiftUI
import Bedrock
// MARK: - Surface Colors
/// Surface colors with warm off-white tones for a professional light theme.
public enum BusinessCardSurfaceColors: SurfaceColorProvider {
/// Primary background - warm off-white base
public static let primary = Color(red: 0.97, green: 0.96, blue: 0.94)
/// Secondary/elevated surface
public static let secondary = Color(red: 0.95, green: 0.95, blue: 0.95)
/// Tertiary/card surface - most elevated
public static let tertiary = Color(red: 1.0, green: 1.0, blue: 1.0)
/// Overlay background (for sheets/modals)
public static let overlay = Color(red: 0.97, green: 0.96, blue: 0.94)
/// Card/grouped element background
public static let card = Color(red: 1.0, green: 1.0, blue: 1.0)
/// Subtle fill for grouped content sections
public static let groupedFill = Color(red: 0.95, green: 0.94, blue: 0.92)
/// Section fill for list sections
public static let sectionFill = Color(red: 0.93, green: 0.92, blue: 0.90)
}
// MARK: - Text Colors
public enum BusinessCardTextColors: TextColorProvider {
public static let primary = Color(red: 0.14, green: 0.14, blue: 0.17)
public static let secondary = Color(red: 0.32, green: 0.34, blue: 0.40)
public static let tertiary = Color(red: 0.56, green: 0.58, blue: 0.62)
public static let disabled = Color(red: 0.70, green: 0.72, blue: 0.75)
public static let placeholder = Color(red: 0.60, green: 0.62, blue: 0.66)
public static let inverse = Color(red: 0.98, green: 0.98, blue: 0.98)
}
// MARK: - Accent Colors
public enum BusinessCardAccentColors: AccentColorProvider {
/// Primary accent - warm red
public static let primary = Color(red: 0.95, green: 0.33, blue: 0.28)
/// Light variant
public static let light = Color(red: 0.98, green: 0.50, blue: 0.45)
/// Dark variant
public static let dark = Color(red: 0.75, green: 0.25, blue: 0.22)
/// Secondary accent - ink/dark
public static let secondary = Color(red: 0.12, green: 0.12, blue: 0.14)
}
// MARK: - Button Colors
public enum BusinessCardButtonColors: ButtonColorProvider {
public static let primaryLight = Color(red: 0.98, green: 0.45, blue: 0.40)
public static let primaryDark = Color(red: 0.85, green: 0.28, blue: 0.24)
public static let secondary = Color(red: 0.14, green: 0.14, blue: 0.17).opacity(Design.Opacity.subtle)
public static let destructive = Color.red.opacity(Design.Opacity.heavy)
public static let cancelText = Color(red: 0.32, green: 0.34, blue: 0.40)
}
// MARK: - Status Colors
public enum BusinessCardStatusColors: StatusColorProvider {
public static let success = Color(red: 0.2, green: 0.75, blue: 0.4)
public static let warning = Color(red: 0.95, green: 0.75, blue: 0.25)
public static let error = Color(red: 0.9, green: 0.3, blue: 0.3)
public static let info = Color(red: 0.3, green: 0.6, blue: 0.9)
}
// MARK: - Border Colors
public enum BusinessCardBorderColors: BorderColorProvider {
public static let subtle = Color(red: 0.14, green: 0.14, blue: 0.17).opacity(Design.Opacity.subtle)
public static let standard = Color(red: 0.14, green: 0.14, blue: 0.17).opacity(Design.Opacity.hint)
public static let emphasized = Color(red: 0.14, green: 0.14, blue: 0.17).opacity(Design.Opacity.light)
public static let selected = BusinessCardAccentColors.primary.opacity(Design.Opacity.medium)
}
// MARK: - Interactive Colors
public enum BusinessCardInteractiveColors: InteractiveColorProvider {
public static let selected = BusinessCardAccentColors.primary.opacity(Design.Opacity.selection)
public static let hover = Color(red: 0.14, green: 0.14, blue: 0.17).opacity(Design.Opacity.subtle)
public static let pressed = Color(red: 0.14, green: 0.14, blue: 0.17).opacity(Design.Opacity.hint)
public static let focus = BusinessCardAccentColors.light
}
// MARK: - Combined Theme
/// BusinessCard's complete color theme.
/// Note: We use the individual color provider typealiases (AppSurface, AppThemeAccent, etc.)
/// directly in views rather than going through the theme type.
///
/// This enum is not used directly but documents the theme structure.
/// The AppColorTheme conformance is omitted to avoid MainActor isolation conflicts.
public enum BusinessCardTheme {
public typealias Surface = BusinessCardSurfaceColors
public typealias Text = BusinessCardTextColors
public typealias Accent = BusinessCardAccentColors
public typealias Button = BusinessCardButtonColors
public typealias Status = BusinessCardStatusColors
public typealias Border = BusinessCardBorderColors
public typealias Interactive = BusinessCardInteractiveColors
}
// MARK: - Convenience Typealiases
/// Short typealiases for cleaner usage throughout the app.
/// These avoid conflicts with Bedrock's default typealiases by using unique names.
///
/// Usage:
/// ```swift
/// .background(AppSurface.primary)
/// .foregroundStyle(AppThemeAccent.primary)
/// ```
typealias AppSurface = BusinessCardSurfaceColors
typealias AppThemeText = BusinessCardTextColors
typealias AppThemeAccent = BusinessCardAccentColors
typealias AppButtonColors = BusinessCardButtonColors
typealias AppStatus = BusinessCardStatusColors
typealias AppBorder = BusinessCardBorderColors
typealias AppInteractive = BusinessCardInteractiveColors

View File

@ -12,5 +12,10 @@
<string>$(CLOUDKIT_CONTAINER_IDENTIFIER)</string>
<key>AppClipDomain</key>
<string>$(APPCLIP_DOMAIN)</string>
<key>UILaunchScreen</key>
<dict>
<key>UIColorName</key>
<string>LaunchBackground</string>
</dict>
</dict>
</plist>

View File

@ -4,6 +4,7 @@ enum AppTab: String, CaseIterable, Hashable, Identifiable {
case cards
case contacts
case widgets
case settings
var id: String { rawValue }
}

View File

@ -93,6 +93,10 @@
},
"Address" : {
},
"App Clip (includes photo)" : {
"comment" : "A title for a section that allows users to generate an App Clip link for their card, including a photo.",
"isCommentAutoGenerated" : true
},
"Apt, Suite, Unit (optional)" : {
@ -115,6 +119,10 @@
},
"Card Label" : {
},
"Card not found" : {
"comment" : "Error message when a requested shared card is not found.",
"isCommentAutoGenerated" : true
},
"Card style" : {
"extractionState" : "stale",
@ -197,6 +205,10 @@
},
"Contact" : {
},
"Could not load card: %@" : {
"comment" : "A description of an error that might occur when fetching a shared card. The argument is text describing the underlying error.",
"isCommentAutoGenerated" : true
},
"Country" : {
@ -232,6 +244,10 @@
},
"Create your first card" : {
},
"Creates a link that opens a mini-app for recipients to preview and save your card with photo." : {
"comment" : "A description of the feature that generates an App Clip link.",
"isCommentAutoGenerated" : true
},
"Custom color" : {
@ -297,9 +313,24 @@
},
"Email or Username" : {
},
"Expires in 7 days" : {
"comment" : "A notice displayed below an App Clip URL that indicates when it will expire.",
"isCommentAutoGenerated" : true
},
"Failed to create share URL" : {
"comment" : "Error message when the share URL for a shared card cannot be created.",
"isCommentAutoGenerated" : true
},
"First Name" : {
},
"Generate App Clip Link" : {
},
"Generate New Link" : {
"comment" : "A button label that resets the generation of an App Clip URL.",
"isCommentAutoGenerated" : true
},
"Header Layout" : {
@ -377,6 +408,10 @@
},
"Maiden Name" : {
},
"Matt Bruce" : {
"comment" : "The name of the developer of the app.",
"isCommentAutoGenerated" : true
},
"Messaging" : {
@ -782,6 +817,10 @@
},
"The default card is used for sharing and widgets." : {
},
"This card has expired" : {
"comment" : "Error message indicating that the shared card has expired.",
"isCommentAutoGenerated" : true
},
"This doesn't appear to be a business card QR code." : {
@ -791,6 +830,13 @@
},
"Title (optional)" : {
},
"Upload failed: %@" : {
"comment" : "A description of an error that might occur when uploading a shared card. The argument is text describing the underlying error.",
"isCommentAutoGenerated" : true
},
"Uploading card" : {
},
"URL" : {

View File

@ -0,0 +1,52 @@
//
// SettingsState.swift
// BusinessCard
//
// Observable state for app settings.
//
import Foundation
import Observation
@Observable
@MainActor
final class SettingsState {
// MARK: - App Info
/// The app's display name.
var appName: String {
Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String
?? "BusinessCard"
}
/// The app's version string (e.g., "1.0.0").
var appVersion: String {
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0"
}
/// The app's build number.
var buildNumber: String {
Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "1"
}
/// Combined version and build string for display.
var versionString: String {
"Version \(appVersion) (\(buildNumber))"
}
// MARK: - Debug Settings
#if DEBUG
/// Debug-only: Simulates premium being unlocked for testing.
var isDebugPremiumEnabled: Bool {
get { UserDefaults.standard.bool(forKey: "debugPremiumEnabled") }
set { UserDefaults.standard.set(newValue, forKey: "debugPremiumEnabled") }
}
#endif
// MARK: - Initialization
init() {}
}

View File

@ -31,6 +31,10 @@ struct RootTabView: View {
Tab(String.localized("Widgets"), systemImage: "square.grid.2x2", value: AppTab.widgets) {
WidgetsView()
}
Tab(String.localized("Settings"), systemImage: "gearshape", value: AppTab.settings) {
SettingsView()
}
}
.sheet(isPresented: $showingShareSheet) {
ShareCardView()

View File

@ -0,0 +1,150 @@
//
// SettingsView.swift
// BusinessCard
//
// App settings screen using Bedrock components.
//
import SwiftUI
import Bedrock
struct SettingsView: View {
@State private var settingsState = SettingsState()
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: Design.Spacing.large) {
// MARK: - About Section
aboutSection
// MARK: - Debug Section
#if DEBUG
debugSection
#endif
Spacer(minLength: Design.Spacing.xxxLarge)
}
.padding(.horizontal, Design.Spacing.large)
.padding(.top, Design.Spacing.medium)
}
.background(Color(.systemGroupedBackground))
.navigationTitle(String.localized("Settings"))
.navigationBarTitleDisplayMode(.large)
}
}
// MARK: - About Section
private var aboutSection: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: String.localized("About"),
systemImage: "info.circle",
accentColor: AppThemeAccent.primary
)
SettingsCard(
backgroundColor: Color(.secondarySystemGroupedBackground),
borderColor: .clear
) {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
// App Name
HStack {
Text(settingsState.appName)
.font(.system(size: Design.BaseFontSize.title, weight: .semibold))
.foregroundStyle(.primary)
Spacer()
}
// Version
HStack {
Text(String.localized("Version"))
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.primary)
Spacer()
Text(settingsState.versionString)
.font(.system(size: Design.BaseFontSize.body, design: .monospaced))
.foregroundStyle(.secondary)
}
// Developer
HStack {
Text(String.localized("Developer"))
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.primary)
Spacer()
Text("Matt Bruce")
.font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.secondary)
}
}
}
}
}
// MARK: - Debug Section
#if DEBUG
private var debugSection: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: "Debug",
systemImage: "ant.fill",
accentColor: AppStatus.error
)
SettingsCard(
backgroundColor: Color(.secondarySystemGroupedBackground),
borderColor: .clear
) {
VStack(spacing: Design.Spacing.medium) {
SettingsToggle(
title: "Enable Debug Premium",
subtitle: "Unlock all premium features for testing",
isOn: Binding(
get: { settingsState.isDebugPremiumEnabled },
set: { settingsState.isDebugPremiumEnabled = $0 }
),
accentColor: AppStatus.warning
)
SettingsNavigationRow(
title: "Icon Generator",
subtitle: "Generate and save app icon to Files",
backgroundColor: Color(.tertiarySystemGroupedBackground)
) {
IconGeneratorView(config: .businessCard, appName: "BusinessCard")
}
SettingsNavigationRow(
title: "Branding Preview",
subtitle: "Preview app icon and launch screen",
backgroundColor: Color(.tertiarySystemGroupedBackground)
) {
BrandingPreviewView(
iconConfig: .businessCard,
launchConfig: .businessCard,
appName: "BusinessCard"
)
}
}
}
}
}
#endif
}
// MARK: - Preview
#Preview {
SettingsView()
}

View File

@ -6,17 +6,9 @@
<array>
<string>appclips:$(APPCLIP_DOMAIN)</string>
</array>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>$(CLOUDKIT_CONTAINER_IDENTIFIER)</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>
<string>CloudKit</string>
</array>
<key>com.apple.developer.parent-application-identifiers</key>
<array>
<string>$(TeamIdentifierPrefix)$(APP_BUNDLE_IDENTIFIER)</string>
<string>$(AppIdentifierPrefix)$(APP_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</plist>