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

This commit is contained in:
Matt Bruce 2026-02-14 16:14:17 -06:00
parent 80439171bb
commit 2b679b0167
5 changed files with 99 additions and 33 deletions

View File

@ -817,10 +817,6 @@
}
}
},
"Show first-run onboarding again on next app launch" : {
"comment" : "A description of the reset onboarding feature.",
"isCommentAutoGenerated" : true
},
"Social Media" : {
},

View File

@ -14,11 +14,20 @@ struct WatchCard: Codable, Identifiable, Hashable {
var isDefault: Bool
/// Pre-generated QR code PNG data from iOS (CoreImage not available on watchOS)
var qrCodeImageData: Data?
/// Pre-generated App Clip URL QR code PNG data
var appClipQRCodeImageData: Data?
/// Returns a SwiftUI Image from the synced QR code data
/// Returns a SwiftUI Image from the synced vCard QR code data
var qrCodeImage: Image? {
guard let data = qrCodeImageData,
let uiImage = UIImage(data: data) else { return nil }
return Image(uiImage: uiImage)
}
/// Returns a SwiftUI Image from the synced App Clip QR code data
var appClipQRCodeImage: Image? {
guard let data = appClipQRCodeImageData,
let uiImage = UIImage(data: data) else { return nil }
return Image(uiImage: uiImage)
}
}

View File

@ -117,6 +117,10 @@
}
}
},
"Swipe left or right to switch QR code type" : {
"comment" : "A hint text that appears when a user has an App Clip QR code and is viewing a vCard QR code. It instructs the user to swipe to switch between the two types of QR codes.",
"isCommentAutoGenerated" : true
},
"Sync from iPhone" : {
"comment" : "A description of how to sync a watch code from the iPhone app.",
"isCommentAutoGenerated" : true

View File

@ -57,7 +57,8 @@ final class WatchConnectivityService: NSObject {
website: syncable.website,
location: syncable.location,
isDefault: syncable.isDefault,
qrCodeImageData: syncable.qrCodeImageData
qrCodeImageData: syncable.qrCodeImageData,
appClipQRCodeImageData: syncable.appClipQRCodeImageData
)
}
WatchDesign.debugLog("WatchConnectivity: Decoded \(watchCards.count) cards")
@ -122,4 +123,5 @@ private struct SyncableCard: Codable, Identifiable {
var twitter: String
var instagram: String
var qrCodeImageData: Data?
var appClipQRCodeImageData: Data?
}

View File

@ -2,12 +2,32 @@ import SwiftUI
struct WatchContentView: View {
@Environment(WatchCardStore.self) private var cardStore
@State private var selectedCardID: UUID?
private var displayCards: [WatchCard] {
cardStore.cards
}
private var defaultSelection: UUID? {
cardStore.defaultCardID ?? displayCards.first?.id
}
var body: some View {
if let card = cardStore.defaultCard {
WatchQRCodeCardView(card: card)
} else {
if displayCards.isEmpty {
WatchEmptyStateView()
} else if displayCards.count == 1 {
WatchQRCodeCardView(card: displayCards[0])
} else {
TabView(selection: Binding(
get: { selectedCardID ?? defaultSelection ?? displayCards[0].id },
set: { selectedCardID = $0 }
)) {
ForEach(displayCards) { card in
WatchQRCodeCardView(card: card)
.tag(card.id)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
}
}
@ -32,12 +52,65 @@ private struct WatchEmptyStateView: View {
}
}
private enum WatchQRMode: String, CaseIterable {
case vCard = "vCard"
case appClip = "App Clip"
}
private struct WatchQRCodeCardView: View {
let card: WatchCard
@State private var selectedQRIndex = 0
private var hasAppClipQR: Bool { card.appClipQRCodeImage != nil }
private var qrPages: [(WatchQRMode, Image?)] {
var pages: [(WatchQRMode, Image?)] = [(.vCard, card.qrCodeImage)]
if hasAppClipQR {
pages.append((.appClip, card.appClipQRCodeImage))
}
return pages
}
var body: some View {
VStack(spacing: WatchDesign.Spacing.medium) {
if let image = card.qrCodeImage {
VStack(spacing: WatchDesign.Spacing.small) {
if qrPages.count > 1 {
// Swipeable QR codes (vCard App Clip) - no buttons, just swipe
TabView(selection: $selectedQRIndex) {
ForEach(Array(qrPages.enumerated()), id: \.offset) { index, item in
qrCodeContent(image: item.1)
.tag(index)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.frame(height: WatchDesign.Size.qrSize + WatchDesign.Spacing.medium * 2)
} else {
// Single QR code
qrCodeContent(image: card.qrCodeImage ?? card.appClipQRCodeImage)
}
VStack(spacing: WatchDesign.Spacing.extraSmall) {
Text(card.fullName)
.font(.headline)
.foregroundStyle(Color.WatchPalette.text)
.lineLimit(1)
Text(card.role)
.font(.caption)
.foregroundStyle(Color.WatchPalette.muted)
.lineLimit(1)
}
}
.padding(WatchDesign.Spacing.medium)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.WatchPalette.background)
.accessibilityElement(children: .ignore)
.accessibilityLabel(String(localized: "QR code"))
.accessibilityValue("\(card.fullName), \(card.role)")
.accessibilityHint(hasAppClipQR ? String(localized: "Swipe left or right to switch QR code type") : "")
}
@ViewBuilder
private func qrCodeContent(image: Image?) -> some View {
if let image {
image
.resizable()
.interpolation(.none)
@ -47,7 +120,6 @@ private struct WatchQRCodeCardView: View {
.background(Color.WatchPalette.card)
.clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.large))
} else {
// Fallback when no QR code synced yet
VStack(spacing: WatchDesign.Spacing.small) {
Image(systemName: "qrcode")
.font(.largeTitle)
@ -60,23 +132,6 @@ private struct WatchQRCodeCardView: View {
.background(Color.WatchPalette.card)
.clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.large))
}
VStack(spacing: WatchDesign.Spacing.extraSmall) {
Text(card.fullName)
.font(.headline)
.foregroundStyle(Color.WatchPalette.text)
Text(card.role)
.font(.caption)
.foregroundStyle(Color.WatchPalette.muted)
}
}
.padding(WatchDesign.Spacing.medium)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.WatchPalette.background)
.accessibilityElement(children: .ignore)
.accessibilityLabel(String(localized: "QR code"))
.accessibilityValue("\(card.fullName), \(card.role)")
}
}