Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
80439171bb
commit
2b679b0167
@ -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" : {
|
"Social Media" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|||||||
@ -14,11 +14,20 @@ struct WatchCard: Codable, Identifiable, Hashable {
|
|||||||
var isDefault: Bool
|
var isDefault: Bool
|
||||||
/// Pre-generated QR code PNG data from iOS (CoreImage not available on watchOS)
|
/// Pre-generated QR code PNG data from iOS (CoreImage not available on watchOS)
|
||||||
var qrCodeImageData: Data?
|
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? {
|
var qrCodeImage: Image? {
|
||||||
guard let data = qrCodeImageData,
|
guard let data = qrCodeImageData,
|
||||||
let uiImage = UIImage(data: data) else { return nil }
|
let uiImage = UIImage(data: data) else { return nil }
|
||||||
return Image(uiImage: uiImage)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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" : {
|
"Sync from iPhone" : {
|
||||||
"comment" : "A description of how to sync a watch code from the iPhone app.",
|
"comment" : "A description of how to sync a watch code from the iPhone app.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
|||||||
@ -57,7 +57,8 @@ final class WatchConnectivityService: NSObject {
|
|||||||
website: syncable.website,
|
website: syncable.website,
|
||||||
location: syncable.location,
|
location: syncable.location,
|
||||||
isDefault: syncable.isDefault,
|
isDefault: syncable.isDefault,
|
||||||
qrCodeImageData: syncable.qrCodeImageData
|
qrCodeImageData: syncable.qrCodeImageData,
|
||||||
|
appClipQRCodeImageData: syncable.appClipQRCodeImageData
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
WatchDesign.debugLog("WatchConnectivity: Decoded \(watchCards.count) cards")
|
WatchDesign.debugLog("WatchConnectivity: Decoded \(watchCards.count) cards")
|
||||||
@ -122,4 +123,5 @@ private struct SyncableCard: Codable, Identifiable {
|
|||||||
var twitter: String
|
var twitter: String
|
||||||
var instagram: String
|
var instagram: String
|
||||||
var qrCodeImageData: Data?
|
var qrCodeImageData: Data?
|
||||||
|
var appClipQRCodeImageData: Data?
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,12 +2,32 @@ import SwiftUI
|
|||||||
|
|
||||||
struct WatchContentView: View {
|
struct WatchContentView: View {
|
||||||
@Environment(WatchCardStore.self) private var cardStore
|
@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 {
|
var body: some View {
|
||||||
if let card = cardStore.defaultCard {
|
if displayCards.isEmpty {
|
||||||
WatchQRCodeCardView(card: card)
|
|
||||||
} else {
|
|
||||||
WatchEmptyStateView()
|
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,43 +52,51 @@ private struct WatchEmptyStateView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum WatchQRMode: String, CaseIterable {
|
||||||
|
case vCard = "vCard"
|
||||||
|
case appClip = "App Clip"
|
||||||
|
}
|
||||||
|
|
||||||
private struct WatchQRCodeCardView: View {
|
private struct WatchQRCodeCardView: View {
|
||||||
let card: WatchCard
|
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 {
|
var body: some View {
|
||||||
VStack(spacing: WatchDesign.Spacing.medium) {
|
VStack(spacing: WatchDesign.Spacing.small) {
|
||||||
if let image = card.qrCodeImage {
|
if qrPages.count > 1 {
|
||||||
image
|
// Swipeable QR codes (vCard ↔ App Clip) - no buttons, just swipe
|
||||||
.resizable()
|
TabView(selection: $selectedQRIndex) {
|
||||||
.interpolation(.none)
|
ForEach(Array(qrPages.enumerated()), id: \.offset) { index, item in
|
||||||
.scaledToFit()
|
qrCodeContent(image: item.1)
|
||||||
.frame(width: WatchDesign.Size.qrSize, height: WatchDesign.Size.qrSize)
|
.tag(index)
|
||||||
.padding(WatchDesign.Spacing.small)
|
}
|
||||||
.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)
|
|
||||||
.foregroundStyle(Color.WatchPalette.muted)
|
|
||||||
Text("Sync from iPhone")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(Color.WatchPalette.muted)
|
|
||||||
}
|
}
|
||||||
.frame(width: WatchDesign.Size.qrSize, height: WatchDesign.Size.qrSize)
|
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||||
.background(Color.WatchPalette.card)
|
.frame(height: WatchDesign.Size.qrSize + WatchDesign.Spacing.medium * 2)
|
||||||
.clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.large))
|
} else {
|
||||||
|
// Single QR code
|
||||||
|
qrCodeContent(image: card.qrCodeImage ?? card.appClipQRCodeImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(spacing: WatchDesign.Spacing.extraSmall) {
|
VStack(spacing: WatchDesign.Spacing.extraSmall) {
|
||||||
Text(card.fullName)
|
Text(card.fullName)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundStyle(Color.WatchPalette.text)
|
.foregroundStyle(Color.WatchPalette.text)
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
Text(card.role)
|
Text(card.role)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(Color.WatchPalette.muted)
|
.foregroundStyle(Color.WatchPalette.muted)
|
||||||
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(WatchDesign.Spacing.medium)
|
.padding(WatchDesign.Spacing.medium)
|
||||||
@ -77,6 +105,33 @@ private struct WatchQRCodeCardView: View {
|
|||||||
.accessibilityElement(children: .ignore)
|
.accessibilityElement(children: .ignore)
|
||||||
.accessibilityLabel(String(localized: "QR code"))
|
.accessibilityLabel(String(localized: "QR code"))
|
||||||
.accessibilityValue("\(card.fullName), \(card.role)")
|
.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)
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(width: WatchDesign.Size.qrSize, height: WatchDesign.Size.qrSize)
|
||||||
|
.padding(WatchDesign.Spacing.small)
|
||||||
|
.background(Color.WatchPalette.card)
|
||||||
|
.clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.large))
|
||||||
|
} else {
|
||||||
|
VStack(spacing: WatchDesign.Spacing.small) {
|
||||||
|
Image(systemName: "qrcode")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.foregroundStyle(Color.WatchPalette.muted)
|
||||||
|
Text("Sync from iPhone")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(Color.WatchPalette.muted)
|
||||||
|
}
|
||||||
|
.frame(width: WatchDesign.Size.qrSize, height: WatchDesign.Size.qrSize)
|
||||||
|
.background(Color.WatchPalette.card)
|
||||||
|
.clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.large))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user