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)