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

This commit is contained in:
Matt Bruce 2026-01-10 13:23:22 -06:00
parent 1262377bcb
commit 54d4561158
4 changed files with 112 additions and 6 deletions

View File

@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
EA837E672F107D6800077F87 /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA837E662F107D6800077F87 /* Bedrock */; };
EAAE892A2F12DE110075BC8A /* BusinessCardWatch Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = EA837F982F11B16400077F87 /* BusinessCardWatch Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -25,8 +26,29 @@
remoteGlobalIDString = EA8379222F105F2600077F87;
remoteInfo = BusinessCard;
};
EAAE892B2F12DE110075BC8A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = EA83791B2F105F2600077F87 /* Project object */;
proxyType = 1;
remoteGlobalIDString = EA837F972F11B16400077F87;
remoteInfo = "BusinessCardWatch Watch App";
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
EAAE892D2F12DE110075BC8A /* Embed Watch Content */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
dstSubfolderSpec = 16;
files = (
EAAE892A2F12DE110075BC8A /* BusinessCardWatch Watch App.app in Embed Watch Content */,
);
name = "Embed Watch Content";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
EA8379232F105F2600077F87 /* BusinessCard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BusinessCard.app; sourceTree = BUILT_PRODUCTS_DIR; };
EA8379302F105F2800077F87 /* BusinessCardTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BusinessCardTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@ -110,6 +132,7 @@
EA8379332F105F2800077F87 /* BusinessCardTests */,
EA83793D2F105F2800077F87 /* BusinessCardUITests */,
EA837F992F11B16400077F87 /* BusinessCardWatch Watch App */,
EAAE89292F12DE110075BC8A /* Frameworks */,
EA8379242F105F2600077F87 /* Products */,
);
sourceTree = "<group>";
@ -125,6 +148,13 @@
name = Products;
sourceTree = "<group>";
};
EAAE89292F12DE110075BC8A /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -135,10 +165,12 @@
EA83791F2F105F2600077F87 /* Sources */,
EA8379202F105F2600077F87 /* Frameworks */,
EA8379212F105F2600077F87 /* Resources */,
EAAE892D2F12DE110075BC8A /* Embed Watch Content */,
);
buildRules = (
);
dependencies = (
EAAE892C2F12DE110075BC8A /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
EA8379252F105F2600077F87 /* BusinessCard */,
@ -345,6 +377,11 @@
target = EA8379222F105F2600077F87 /* BusinessCard */;
targetProxy = EA83793B2F105F2800077F87 /* PBXContainerItemProxy */;
};
EAAE892C2F12DE110075BC8A /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = EA837F972F11B16400077F87 /* BusinessCardWatch Watch App */;
targetProxy = EAAE892B2F12DE110075BC8A /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */

View File

@ -59,7 +59,9 @@ final class WatchConnectivityService: NSObject {
}
private func sendCardsToWatch(_ cards: [BusinessCard]) {
Design.debugLog("WatchConnectivity: Syncing \(cards.count) cards to Watch")
guard let session = session else { return }
Design.debugLog("WatchConnectivity: Syncing \(cards.count) cards to Watch (reachable: \(session.isReachable))")
let syncableCards = cards.map { card in
createSyncableCard(from: card)
@ -67,12 +69,26 @@ final class WatchConnectivityService: NSObject {
do {
let encoded = try JSONEncoder().encode(syncableCards)
let userInfo: [String: Any] = ["cards": encoded]
let message: [String: Any] = ["cards": encoded]
// Use transferUserInfo instead of updateApplicationContext
// This queues the transfer and works even in debug mode
session?.transferUserInfo(userInfo)
Design.debugLog("WatchConnectivity: Queued \(encoded.count) bytes via transferUserInfo")
// ALWAYS update application context - this persists and will be available
// when watch app launches, even if not currently reachable
do {
try session.updateApplicationContext(message)
Design.debugLog("WatchConnectivity: Updated application context with \(encoded.count) bytes")
} catch {
Design.debugLog("WatchConnectivity: updateApplicationContext failed: \(error)")
}
// Also try immediate delivery if reachable
if session.isReachable {
session.sendMessage(message, replyHandler: nil) { error in
Task { @MainActor in
Design.debugLog("WatchConnectivity: sendMessage failed: \(error)")
}
}
Design.debugLog("WatchConnectivity: Also sent via sendMessage (reachable)")
}
} catch {
Design.debugLog("WatchConnectivity: ERROR - Failed to encode: \(error)")
}
@ -81,6 +97,8 @@ final class WatchConnectivityService: NSObject {
fileprivate func handleActivation() {
isActivated = true
Design.debugLog("WatchConnectivity: Activation complete - paired: \(session?.isPaired ?? false), installed: \(session?.isWatchAppInstalled ?? false), reachable: \(session?.isReachable ?? false)")
// Send any pending cards that were queued before activation
if let cards = pendingCards {
Design.debugLog("WatchConnectivity: Sending \(cards.count) queued cards after activation")
@ -89,6 +107,17 @@ final class WatchConnectivityService: NSObject {
}
}
/// Force a retry - call this after watch app is confirmed running
func retrySyncIfNeeded() {
guard let session = session else { return }
Design.debugLog("WatchConnectivity: Manual retry - paired: \(session.isPaired), installed: \(session.isWatchAppInstalled), reachable: \(session.isReachable)")
if let cards = pendingCards {
pendingCards = nil
syncCards(cards)
}
}
private func createSyncableCard(from card: BusinessCard) -> SyncableCard {
// Get first email, phone, website, location from contact fields
let email = card.firstContactField(ofType: "email")?.value ?? ""

View File

@ -96,6 +96,13 @@ extension WatchConnectivityService: WCSessionDelegate {
processReceivedContext(userInfo)
}
}
nonisolated func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
Task { @MainActor in
WatchDesign.debugLog("WatchConnectivity: Received message")
processReceivedContext(message)
}
}
}
/// Syncable card structure matching iOS side (for decoding)

View File

@ -16,7 +16,40 @@ final class WatchCardStore {
init() {
loadDefaultID()
WatchDesign.debugLog("WatchCardStore: Initialized, waiting for cards from iPhone")
// Set up callback for when cards are received from iPhone
WatchConnectivityService.shared.onCardsReceived = { [weak self] cards in
self?.updateCards(cards)
}
#if targetEnvironment(simulator)
// Load sample data on simulator since WatchConnectivity often doesn't work
if cards.isEmpty {
loadSimulatorSampleData()
}
#endif
}
#if targetEnvironment(simulator)
private func loadSimulatorSampleData() {
WatchDesign.debugLog("WatchCardStore: Loading simulator sample data")
cards = [
WatchCard(
id: UUID(),
displayName: "Test User",
role: "iOS Developer",
company: "Sample Corp",
email: "test@example.com",
phone: "+1 555-0123",
website: "example.com",
location: "San Francisco, CA",
isDefault: true,
qrCodeImageData: nil
)
]
defaultCardID = cards.first?.id
}
#endif
var defaultCard: WatchCard? {
guard let defaultCardID else { return cards.first(where: { $0.isDefault }) ?? cards.first }