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

View File

@ -59,7 +59,9 @@ final class WatchConnectivityService: NSObject {
} }
private func sendCardsToWatch(_ cards: [BusinessCard]) { 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 let syncableCards = cards.map { card in
createSyncableCard(from: card) createSyncableCard(from: card)
@ -67,12 +69,26 @@ final class WatchConnectivityService: NSObject {
do { do {
let encoded = try JSONEncoder().encode(syncableCards) let encoded = try JSONEncoder().encode(syncableCards)
let userInfo: [String: Any] = ["cards": encoded] let message: [String: Any] = ["cards": encoded]
// Use transferUserInfo instead of updateApplicationContext // ALWAYS update application context - this persists and will be available
// This queues the transfer and works even in debug mode // when watch app launches, even if not currently reachable
session?.transferUserInfo(userInfo) do {
Design.debugLog("WatchConnectivity: Queued \(encoded.count) bytes via transferUserInfo") 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 { } catch {
Design.debugLog("WatchConnectivity: ERROR - Failed to encode: \(error)") Design.debugLog("WatchConnectivity: ERROR - Failed to encode: \(error)")
} }
@ -81,6 +97,8 @@ final class WatchConnectivityService: NSObject {
fileprivate func handleActivation() { fileprivate func handleActivation() {
isActivated = true 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 // Send any pending cards that were queued before activation
if let cards = pendingCards { if let cards = pendingCards {
Design.debugLog("WatchConnectivity: Sending \(cards.count) queued cards after activation") 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 { private func createSyncableCard(from card: BusinessCard) -> SyncableCard {
// Get first email, phone, website, location from contact fields // Get first email, phone, website, location from contact fields
let email = card.firstContactField(ofType: "email")?.value ?? "" let email = card.firstContactField(ofType: "email")?.value ?? ""

View File

@ -96,6 +96,13 @@ extension WatchConnectivityService: WCSessionDelegate {
processReceivedContext(userInfo) 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) /// Syncable card structure matching iOS side (for decoding)

View File

@ -16,7 +16,40 @@ final class WatchCardStore {
init() { init() {
loadDefaultID() loadDefaultID()
WatchDesign.debugLog("WatchCardStore: Initialized, waiting for cards from iPhone") 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? { var defaultCard: WatchCard? {
guard let defaultCardID else { return cards.first(where: { $0.isDefault }) ?? cards.first } guard let defaultCardID else { return cards.first(where: { $0.isDefault }) ?? cards.first }