Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
44724914b4
commit
6b10f8b398
@ -629,6 +629,7 @@
|
|||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
|
"INFOPLIST_KEY_UILaunchScreen_BackgroundColor" = LaunchBackground;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@ -665,6 +666,7 @@
|
|||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
|
"INFOPLIST_KEY_UILaunchScreen_BackgroundColor" = LaunchBackground;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@ -851,6 +853,7 @@
|
|||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
|
"INFOPLIST_KEY_UILaunchScreen_BackgroundColor" = LaunchBackground;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -888,6 +891,7 @@
|
|||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
|
"INFOPLIST_KEY_UILaunchScreen_BackgroundColor" = LaunchBackground;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
|||||||
@ -7,12 +7,12 @@
|
|||||||
<key>BusinessCard.xcscheme_^#shared#^_</key>
|
<key>BusinessCard.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>2</integer>
|
<integer>1</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>BusinessCardClip.xcscheme_^#shared#^_</key>
|
<key>BusinessCardClip.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>1</integer>
|
<integer>2</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>BusinessCardWatch Watch App.xcscheme_^#shared#^_</key>
|
<key>BusinessCardWatch Watch App.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
BIN
BusinessCard/Assets.xcassets/AppIcon.appiconset/AppIcon.png
Normal file
BIN
BusinessCard/Assets.xcassets/AppIcon.appiconset/AppIcon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
|
"filename" : "AppIcon.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"platform" : "ios",
|
"platform" : "ios",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -72,10 +72,18 @@ struct BusinessCardApp: App {
|
|||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
|
ZStack {
|
||||||
|
// Base background matching launch screen - prevents white flash
|
||||||
|
Color.Branding.primary
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
AppLaunchView(config: .businessCard) {
|
||||||
RootTabView()
|
RootTabView()
|
||||||
.environment(appState)
|
.environment(appState)
|
||||||
.preferredColorScheme(.light)
|
.preferredColorScheme(.light)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.modelContainer(modelContainer)
|
.modelContainer(modelContainer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
63
BusinessCard/Design/BrandingConfig.swift
Normal file
63
BusinessCard/Design/BrandingConfig.swift
Normal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
136
BusinessCard/Design/BusinessCardTheme.swift
Normal file
136
BusinessCard/Design/BusinessCardTheme.swift
Normal 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
|
||||||
@ -12,5 +12,10 @@
|
|||||||
<string>$(CLOUDKIT_CONTAINER_IDENTIFIER)</string>
|
<string>$(CLOUDKIT_CONTAINER_IDENTIFIER)</string>
|
||||||
<key>AppClipDomain</key>
|
<key>AppClipDomain</key>
|
||||||
<string>$(APPCLIP_DOMAIN)</string>
|
<string>$(APPCLIP_DOMAIN)</string>
|
||||||
|
<key>UILaunchScreen</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIColorName</key>
|
||||||
|
<string>LaunchBackground</string>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ enum AppTab: String, CaseIterable, Hashable, Identifiable {
|
|||||||
case cards
|
case cards
|
||||||
case contacts
|
case contacts
|
||||||
case widgets
|
case widgets
|
||||||
|
case settings
|
||||||
|
|
||||||
var id: String { rawValue }
|
var id: String { rawValue }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,6 +93,10 @@
|
|||||||
},
|
},
|
||||||
"Address" : {
|
"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)" : {
|
"Apt, Suite, Unit (optional)" : {
|
||||||
|
|
||||||
@ -115,6 +119,10 @@
|
|||||||
},
|
},
|
||||||
"Card Label" : {
|
"Card Label" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Card not found" : {
|
||||||
|
"comment" : "Error message when a requested shared card is not found.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Card style" : {
|
"Card style" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
@ -197,6 +205,10 @@
|
|||||||
},
|
},
|
||||||
"Contact" : {
|
"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" : {
|
"Country" : {
|
||||||
|
|
||||||
@ -232,6 +244,10 @@
|
|||||||
},
|
},
|
||||||
"Create your first card" : {
|
"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" : {
|
"Custom color" : {
|
||||||
|
|
||||||
@ -297,9 +313,24 @@
|
|||||||
},
|
},
|
||||||
"Email or Username" : {
|
"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" : {
|
"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" : {
|
"Header Layout" : {
|
||||||
|
|
||||||
@ -377,6 +408,10 @@
|
|||||||
},
|
},
|
||||||
"Maiden Name" : {
|
"Maiden Name" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Matt Bruce" : {
|
||||||
|
"comment" : "The name of the developer of the app.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Messaging" : {
|
"Messaging" : {
|
||||||
|
|
||||||
@ -782,6 +817,10 @@
|
|||||||
},
|
},
|
||||||
"The default card is used for sharing and widgets." : {
|
"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." : {
|
"This doesn't appear to be a business card QR code." : {
|
||||||
|
|
||||||
@ -791,6 +830,13 @@
|
|||||||
},
|
},
|
||||||
"Title (optional)" : {
|
"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" : {
|
"URL" : {
|
||||||
|
|
||||||
|
|||||||
52
BusinessCard/State/SettingsState.swift
Normal file
52
BusinessCard/State/SettingsState.swift
Normal 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() {}
|
||||||
|
}
|
||||||
@ -31,6 +31,10 @@ struct RootTabView: View {
|
|||||||
Tab(String.localized("Widgets"), systemImage: "square.grid.2x2", value: AppTab.widgets) {
|
Tab(String.localized("Widgets"), systemImage: "square.grid.2x2", value: AppTab.widgets) {
|
||||||
WidgetsView()
|
WidgetsView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Tab(String.localized("Settings"), systemImage: "gearshape", value: AppTab.settings) {
|
||||||
|
SettingsView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingShareSheet) {
|
.sheet(isPresented: $showingShareSheet) {
|
||||||
ShareCardView()
|
ShareCardView()
|
||||||
|
|||||||
150
BusinessCard/Views/SettingsView.swift
Normal file
150
BusinessCard/Views/SettingsView.swift
Normal 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()
|
||||||
|
}
|
||||||
@ -6,17 +6,9 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>appclips:$(APPCLIP_DOMAIN)</string>
|
<string>appclips:$(APPCLIP_DOMAIN)</string>
|
||||||
</array>
|
</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>
|
<key>com.apple.developer.parent-application-identifiers</key>
|
||||||
<array>
|
<array>
|
||||||
<string>$(TeamIdentifierPrefix)$(APP_BUNDLE_IDENTIFIER)</string>
|
<string>$(AppIdentifierPrefix)$(APP_BUNDLE_IDENTIFIER)</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user