diff --git a/BusinessCard.xcodeproj/project.pbxproj b/BusinessCard.xcodeproj/project.pbxproj
index fe643c9..d49680f 100644
--- a/BusinessCard.xcodeproj/project.pbxproj
+++ b/BusinessCard.xcodeproj/project.pbxproj
@@ -532,7 +532,7 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BusinessCard/Info.plist;
INFOPLIST_KEY_NSContactsUsageDescription = "for testing purposes";
- INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "";
+ INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "BusinessCard uses your photo library to add a profile photo to your business card.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -568,7 +568,7 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BusinessCard/Info.plist;
INFOPLIST_KEY_NSContactsUsageDescription = "for testing purposes";
- INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "";
+ INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "BusinessCard uses your photo library to add a profile photo to your business card.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
diff --git a/BusinessCard.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist b/BusinessCard.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist
index e1acda6..a47ee2b 100644
--- a/BusinessCard.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/BusinessCard.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -7,12 +7,12 @@
BusinessCard.xcscheme_^#shared#^_
orderHint
- 1
+ 2
BusinessCardWatch.xcscheme_^#shared#^_
orderHint
- 0
+ 1
diff --git a/BusinessCard/BusinessCardApp.swift b/BusinessCard/BusinessCardApp.swift
index dc0d040..89e31e4 100644
--- a/BusinessCard/BusinessCardApp.swift
+++ b/BusinessCard/BusinessCardApp.swift
@@ -98,6 +98,7 @@ struct BusinessCardApp: App {
WindowGroup {
RootTabView()
.environment(appState)
+ .preferredColorScheme(.light)
}
.modelContainer(modelContainer)
}
diff --git a/BusinessCard/Info.plist b/BusinessCard/Info.plist
index ca9a074..0fc02a1 100644
--- a/BusinessCard/Info.plist
+++ b/BusinessCard/Info.plist
@@ -2,6 +2,10 @@
+ NSCameraUsageDescription
+ BusinessCard uses the camera to scan QR codes on other people's business cards.
+ NSPhotoLibraryUsageDescription
+ BusinessCard uses your photo library to add a profile photo to your business card.
UIBackgroundModes
remote-notification
diff --git a/BusinessCard/Models/CardTheme.swift b/BusinessCard/Models/CardTheme.swift
index 34a1b98..a42ce20 100644
--- a/BusinessCard/Models/CardTheme.swift
+++ b/BusinessCard/Models/CardTheme.swift
@@ -21,7 +21,19 @@ enum CardTheme: String, CaseIterable, Identifiable, Hashable, Sendable {
static var all: [CardTheme] { allCases }
- // RGB values - nonisolated
+ // MARK: - Theme Brightness
+
+ /// Whether this theme requires dark text for proper contrast.
+ /// Light backgrounds need dark text; dark backgrounds need light text.
+ var requiresDarkText: Bool {
+ switch self {
+ case .lime: return true
+ case .coral, .midnight, .ocean, .violet: return false
+ }
+ }
+
+ // MARK: - RGB Values (nonisolated)
+
private var primaryRGB: (Double, Double, Double) {
switch self {
case .coral: return (0.95, 0.35, 0.33)
@@ -52,7 +64,14 @@ enum CardTheme: String, CaseIterable, Identifiable, Hashable, Sendable {
}
}
- // Colors - computed from RGB
+ private var textRGB: (Double, Double, Double) {
+ requiresDarkText
+ ? (0.14, 0.14, 0.17) // Dark text for light backgrounds
+ : (0.98, 0.98, 0.98) // Light text for dark backgrounds
+ }
+
+ // MARK: - Colors (MainActor)
+
@MainActor var primaryColor: Color {
Color(red: primaryRGB.0, green: primaryRGB.1, blue: primaryRGB.2)
}
@@ -64,4 +83,9 @@ enum CardTheme: String, CaseIterable, Identifiable, Hashable, Sendable {
@MainActor var accentColor: Color {
Color(red: accentRGB.0, green: accentRGB.1, blue: accentRGB.2)
}
+
+ /// The appropriate text color for content displayed on this theme's background.
+ @MainActor var textColor: Color {
+ Color(red: textRGB.0, green: textRGB.1, blue: textRGB.2)
+ }
}
diff --git a/BusinessCard/Resources/Localizable.xcstrings b/BusinessCard/Resources/Localizable.xcstrings
index bb48381..241eeaf 100644
--- a/BusinessCard/Resources/Localizable.xcstrings
+++ b/BusinessCard/Resources/Localizable.xcstrings
@@ -1013,9 +1013,6 @@
}
}
}
- },
- "Record who received your card" : {
-
},
"Reviews" : {
"extractionState" : "stale",
@@ -1553,9 +1550,6 @@
},
"This person will appear in your Contacts tab so you can track who has your card." : {
- },
- "Track this share" : {
-
},
"Track who receives your card" : {
"extractionState" : "stale",
diff --git a/BusinessCard/Views/BusinessCardView.swift b/BusinessCard/Views/BusinessCardView.swift
index a94c1ab..566cab9 100644
--- a/BusinessCard/Views/BusinessCardView.swift
+++ b/BusinessCard/Views/BusinessCardView.swift
@@ -47,7 +47,7 @@ private struct StackedCardLayout: View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
CardHeaderView(card: card)
Divider()
- .overlay(Color.Text.inverted.opacity(Design.Opacity.medium))
+ .overlay(card.theme.textColor.opacity(Design.Opacity.medium))
CardDetailsView(card: card)
if card.hasSocialLinks {
SocialLinksRow(card: card)
@@ -69,7 +69,7 @@ private struct SplitCardLayout: View {
}
}
Spacer(minLength: Design.Spacing.medium)
- AccentBlockView(color: card.theme.accentColor)
+ AccentBlockView(color: card.theme.accentColor, textColor: card.theme.textColor)
}
}
}
@@ -90,7 +90,8 @@ private struct PhotoCardLayout: View {
AvatarBadgeView(
systemName: card.avatarSystemName,
accentColor: card.theme.accentColor,
- photoData: card.photoData
+ photoData: card.photoData,
+ borderColor: card.theme.textColor
)
}
}
@@ -100,58 +101,63 @@ private struct PhotoCardLayout: View {
private struct CardHeaderView: View {
let card: BusinessCard
+
+ private var textColor: Color { card.theme.textColor }
var body: some View {
HStack(spacing: Design.Spacing.medium) {
AvatarBadgeView(
systemName: card.avatarSystemName,
accentColor: card.theme.accentColor,
- photoData: card.photoData
+ photoData: card.photoData,
+ borderColor: textColor
)
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
HStack(spacing: Design.Spacing.xSmall) {
Text(card.displayName)
.font(.headline)
.bold()
- .foregroundStyle(Color.Text.inverted)
+ .foregroundStyle(textColor)
if !card.pronouns.isEmpty {
Text("(\(card.pronouns))")
.font(.caption)
- .foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.strong))
+ .foregroundStyle(textColor.opacity(Design.Opacity.strong))
}
}
Text(card.role)
.font(.subheadline)
- .foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.almostFull))
+ .foregroundStyle(textColor.opacity(Design.Opacity.almostFull))
Text(card.company)
.font(.caption)
- .foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.medium))
+ .foregroundStyle(textColor.opacity(Design.Opacity.medium))
}
Spacer(minLength: Design.Spacing.small)
- LabelBadgeView(label: card.label, accentColor: card.theme.accentColor)
+ LabelBadgeView(label: card.label, accentColor: card.theme.accentColor, textColor: textColor)
}
}
}
private struct CardDetailsView: View {
let card: BusinessCard
+
+ private var textColor: Color { card.theme.textColor }
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
if !card.email.isEmpty {
- IconRowView(systemImage: "envelope", text: card.email)
+ IconRowView(systemImage: "envelope", text: card.email, textColor: textColor)
}
if !card.phone.isEmpty {
- IconRowView(systemImage: "phone", text: card.phone)
+ IconRowView(systemImage: "phone", text: card.phone, textColor: textColor)
}
if !card.website.isEmpty {
- IconRowView(systemImage: "link", text: card.website)
+ IconRowView(systemImage: "link", text: card.website, textColor: textColor)
}
if !card.bio.isEmpty {
Text(card.bio)
.font(.caption)
- .foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.strong))
+ .foregroundStyle(textColor.opacity(Design.Opacity.strong))
.lineLimit(2)
.padding(.top, Design.Spacing.xxSmall)
}
@@ -162,25 +168,27 @@ private struct CardDetailsView: View {
private struct SocialLinksRow: View {
let card: BusinessCard
+ private var textColor: Color { card.theme.textColor }
+
var body: some View {
HStack(spacing: Design.Spacing.small) {
if !card.linkedIn.isEmpty {
- SocialIconView(systemImage: "link")
+ SocialIconView(systemImage: "link", textColor: textColor)
}
if !card.twitter.isEmpty {
- SocialIconView(systemImage: "at")
+ SocialIconView(systemImage: "at", textColor: textColor)
}
if !card.instagram.isEmpty {
- SocialIconView(systemImage: "camera")
+ SocialIconView(systemImage: "camera", textColor: textColor)
}
if !card.facebook.isEmpty {
- SocialIconView(systemImage: "person.2")
+ SocialIconView(systemImage: "person.2", textColor: textColor)
}
if !card.tiktok.isEmpty {
- SocialIconView(systemImage: "play.rectangle")
+ SocialIconView(systemImage: "play.rectangle", textColor: textColor)
}
if !card.github.isEmpty {
- SocialIconView(systemImage: "chevron.left.forwardslash.chevron.right")
+ SocialIconView(systemImage: "chevron.left.forwardslash.chevron.right", textColor: textColor)
}
}
.padding(.top, Design.Spacing.xxSmall)
@@ -191,19 +199,21 @@ private struct SocialLinksRow: View {
private struct SocialIconView: View {
let systemImage: String
+ var textColor: Color = Color.Text.inverted
var body: some View {
Image(systemName: systemImage)
.font(.caption2)
- .foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.strong))
+ .foregroundStyle(textColor.opacity(Design.Opacity.strong))
.frame(width: Design.Spacing.xLarge, height: Design.Spacing.xLarge)
- .background(Color.Text.inverted.opacity(Design.Opacity.hint))
+ .background(textColor.opacity(Design.Opacity.hint))
.clipShape(.circle)
}
}
private struct AccentBlockView: View {
let color: Color
+ var textColor: Color = Color.Text.inverted
var body: some View {
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
@@ -211,7 +221,7 @@ private struct AccentBlockView: View {
.frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize)
.overlay(
Image(systemName: "bolt.fill")
- .foregroundStyle(Color.Text.inverted)
+ .foregroundStyle(textColor)
)
}
}
diff --git a/BusinessCard/Views/CardEditorView.swift b/BusinessCard/Views/CardEditorView.swift
index 1cca0da..9389852 100644
--- a/BusinessCard/Views/CardEditorView.swift
+++ b/BusinessCard/Views/CardEditorView.swift
@@ -294,19 +294,23 @@ private struct PhotoPickerRow: View {
let avatarSystemName: String
var body: some View {
+ let hasPhoto = photoData != nil
+ let labelText = hasPhoto ? String.localized("Change Photo") : String.localized("Add Photo")
+ let accentColor = Color.Accent.red
+
HStack(spacing: Design.Spacing.medium) {
AvatarBadgeView(
systemName: avatarSystemName,
- accentColor: Color.Accent.red,
+ accentColor: accentColor,
photoData: photoData
)
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
PhotosPicker(selection: $selectedPhoto, matching: .images) {
- Text(photoData == nil ? String.localized("Add Photo") : String.localized("Change Photo"))
- .foregroundStyle(Color.Accent.red)
+ Text(labelText)
+ .foregroundStyle(accentColor)
}
- if photoData != nil {
+ if hasPhoto {
Button(String.localized("Remove Photo"), role: .destructive) {
photoData = nil
selectedPhoto = nil
@@ -349,21 +353,24 @@ private struct EditorCardPreview: View {
let theme: CardTheme
let photoData: Data?
+ private var textColor: Color { theme.textColor }
+
var body: some View {
VStack(spacing: Design.Spacing.medium) {
HStack(spacing: Design.Spacing.medium) {
AvatarBadgeView(
systemName: avatarSystemName,
accentColor: theme.accentColor,
- photoData: photoData
+ photoData: photoData,
+ borderColor: textColor
)
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
- Text(displayName).font(.headline).bold().foregroundStyle(Color.Text.inverted)
- Text(role).font(.subheadline).foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.almostFull))
- Text(company).font(.caption).foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.medium))
+ Text(displayName).font(.headline).bold().foregroundStyle(textColor)
+ Text(role).font(.subheadline).foregroundStyle(textColor.opacity(Design.Opacity.almostFull))
+ Text(company).font(.caption).foregroundStyle(textColor.opacity(Design.Opacity.medium))
}
Spacer(minLength: Design.Spacing.small)
- LabelBadgeView(label: label, accentColor: theme.accentColor)
+ LabelBadgeView(label: label, accentColor: theme.accentColor, textColor: textColor)
}
}
.padding(Design.Spacing.large)
diff --git a/BusinessCard/Views/Components/AvatarBadgeView.swift b/BusinessCard/Views/Components/AvatarBadgeView.swift
index 9903b42..80670eb 100644
--- a/BusinessCard/Views/Components/AvatarBadgeView.swift
+++ b/BusinessCard/Views/Components/AvatarBadgeView.swift
@@ -8,17 +8,20 @@ struct AvatarBadgeView: View {
let accentColor: Color
let photoData: Data?
let size: CGFloat
+ let borderColor: Color
init(
systemName: String = "person.crop.circle",
accentColor: Color = Color.Accent.red,
photoData: Data? = nil,
- size: CGFloat = Design.CardSize.avatarSize
+ size: CGFloat = Design.CardSize.avatarSize,
+ borderColor: Color = Color.Text.inverted
) {
self.systemName = systemName
self.accentColor = accentColor
self.photoData = photoData
self.size = size
+ self.borderColor = borderColor
}
var body: some View {
@@ -30,11 +33,11 @@ struct AvatarBadgeView: View {
.clipShape(.circle)
.overlay(
Circle()
- .stroke(Color.Text.inverted.opacity(Design.Opacity.medium), lineWidth: Design.LineWidth.thin)
+ .stroke(borderColor.opacity(Design.Opacity.medium), lineWidth: Design.LineWidth.thin)
)
} else {
Circle()
- .fill(Color.Text.inverted)
+ .fill(borderColor)
.frame(width: size, height: size)
.overlay(
Image(systemName: systemName)
@@ -42,7 +45,7 @@ struct AvatarBadgeView: View {
)
.overlay(
Circle()
- .stroke(Color.Text.inverted.opacity(Design.Opacity.medium), lineWidth: Design.LineWidth.thin)
+ .stroke(borderColor.opacity(Design.Opacity.medium), lineWidth: Design.LineWidth.thin)
)
}
}
@@ -58,7 +61,8 @@ struct AvatarBadgeView: View {
AvatarBadgeView(
systemName: "briefcase.fill",
accentColor: Color.CardPalette.ocean,
- size: 80
+ size: 80,
+ borderColor: Color.Text.primary
)
}
.padding()
diff --git a/BusinessCard/Views/Components/IconRowView.swift b/BusinessCard/Views/Components/IconRowView.swift
index f230b26..8da29c7 100644
--- a/BusinessCard/Views/Components/IconRowView.swift
+++ b/BusinessCard/Views/Components/IconRowView.swift
@@ -2,11 +2,11 @@ import SwiftUI
import Bedrock
/// A row with an icon and text, used for contact info display.
-/// Supports both inverted (on dark) and standard (on light) styles.
+/// Accepts a text color for flexibility with different background themes.
struct IconRowView: View {
let systemImage: String
let text: String
- var inverted: Bool = true
+ var textColor: Color = Color.Text.inverted
var body: some View {
HStack(spacing: Design.Spacing.xSmall) {
@@ -19,17 +19,13 @@ struct IconRowView: View {
.lineLimit(1)
}
}
-
- private var textColor: Color {
- inverted ? Color.Text.inverted : Color.Text.primary
- }
}
#Preview {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
IconRowView(systemImage: "envelope", text: "hello@example.com")
IconRowView(systemImage: "phone", text: "+1 555 123 4567")
- IconRowView(systemImage: "link", text: "example.com", inverted: false)
+ IconRowView(systemImage: "link", text: "example.com", textColor: Color.Text.primary)
}
.padding()
.background(Color.CardPalette.midnight)
diff --git a/BusinessCard/Views/Components/LabelBadgeView.swift b/BusinessCard/Views/Components/LabelBadgeView.swift
index 06afd5a..4660950 100644
--- a/BusinessCard/Views/Components/LabelBadgeView.swift
+++ b/BusinessCard/Views/Components/LabelBadgeView.swift
@@ -5,12 +5,13 @@ import Bedrock
struct LabelBadgeView: View {
let label: String
let accentColor: Color
+ var textColor: Color = Color.Text.inverted
var body: some View {
Text(String.localized(label))
.font(.caption)
.bold()
- .foregroundStyle(Color.Text.inverted)
+ .foregroundStyle(textColor)
.padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xxSmall)
.background(accentColor.opacity(Design.Opacity.medium))
@@ -21,7 +22,7 @@ struct LabelBadgeView: View {
#Preview {
HStack {
LabelBadgeView(label: "Work", accentColor: Color.CardPalette.coral)
- LabelBadgeView(label: "Personal", accentColor: Color.CardPalette.ocean)
+ LabelBadgeView(label: "Personal", accentColor: Color.CardPalette.ocean, textColor: Color.Text.primary)
}
.padding()
.background(Color.CardPalette.midnight)
diff --git a/BusinessCard/Views/ShareCardView.swift b/BusinessCard/Views/ShareCardView.swift
index f5b71b9..6ef2a38 100644
--- a/BusinessCard/Views/ShareCardView.swift
+++ b/BusinessCard/Views/ShareCardView.swift
@@ -81,6 +81,8 @@ struct ShareCardView: View {
private struct QRCodeCardView: View {
let card: BusinessCard
+
+ private var textColor: Color { card.theme.textColor }
var body: some View {
VStack(spacing: Design.Spacing.medium) {
@@ -92,7 +94,7 @@ private struct QRCodeCardView: View {
Text("Point your camera at the QR code to receive the card")
.font(.subheadline)
- .foregroundStyle(Color.Text.secondary)
+ .foregroundStyle(textColor.opacity(Design.Opacity.strong))
.multilineTextAlignment(.center)
}
.padding(Design.Spacing.large)