Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
bd6495e1e5
commit
1262377bcb
@ -28,7 +28,6 @@
|
|||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
60186E73BC8040538616865B /* BusinessCardWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BusinessCardWatch.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
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; };
|
||||||
EA83793A2F105F2800077F87 /* BusinessCardUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BusinessCardUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
EA83793A2F105F2800077F87 /* BusinessCardUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BusinessCardUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@ -72,13 +71,6 @@
|
|||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
93EDDE26B3EB4E32AF5B58FC /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
EA8379202F105F2600077F87 /* Frameworks */ = {
|
EA8379202F105F2600077F87 /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@ -126,7 +118,6 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
EA8379232F105F2600077F87 /* BusinessCard.app */,
|
EA8379232F105F2600077F87 /* BusinessCard.app */,
|
||||||
60186E73BC8040538616865B /* BusinessCardWatch.app */,
|
|
||||||
EA8379302F105F2800077F87 /* BusinessCardTests.xctest */,
|
EA8379302F105F2800077F87 /* BusinessCardTests.xctest */,
|
||||||
EA83793A2F105F2800077F87 /* BusinessCardUITests.xctest */,
|
EA83793A2F105F2800077F87 /* BusinessCardUITests.xctest */,
|
||||||
EA837F982F11B16400077F87 /* BusinessCardWatch Watch App.app */,
|
EA837F982F11B16400077F87 /* BusinessCardWatch Watch App.app */,
|
||||||
@ -137,25 +128,6 @@
|
|||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
D007169724A44109B518B9E6 /* BusinessCardWatch */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = 3873468A4B2043BDAA689772 /* Build configuration list for PBXNativeTarget "BusinessCardWatch" */;
|
|
||||||
buildPhases = (
|
|
||||||
7D1EBA94A23F41D5A441C5E4 /* Sources */,
|
|
||||||
93EDDE26B3EB4E32AF5B58FC /* Frameworks */,
|
|
||||||
9F6436BCE5F34967B6A4509D /* Resources */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
);
|
|
||||||
name = BusinessCardWatch;
|
|
||||||
packageProductDependencies = (
|
|
||||||
);
|
|
||||||
productName = BusinessCardWatch;
|
|
||||||
productReference = 60186E73BC8040538616865B /* BusinessCardWatch.app */;
|
|
||||||
productType = "com.apple.product-type.application.watchapp2";
|
|
||||||
};
|
|
||||||
EA8379222F105F2600077F87 /* BusinessCard */ = {
|
EA8379222F105F2600077F87 /* BusinessCard */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = EA8379442F105F2800077F87 /* Build configuration list for PBXNativeTarget "BusinessCard" */;
|
buildConfigurationList = EA8379442F105F2800077F87 /* Build configuration list for PBXNativeTarget "BusinessCard" */;
|
||||||
@ -257,9 +229,6 @@
|
|||||||
LastSwiftUpdateCheck = 2600;
|
LastSwiftUpdateCheck = 2600;
|
||||||
LastUpgradeCheck = 2600;
|
LastUpgradeCheck = 2600;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
D007169724A44109B518B9E6 = {
|
|
||||||
CreatedOnToolsVersion = 26.0;
|
|
||||||
};
|
|
||||||
EA8379222F105F2600077F87 = {
|
EA8379222F105F2600077F87 = {
|
||||||
CreatedOnToolsVersion = 26.0;
|
CreatedOnToolsVersion = 26.0;
|
||||||
};
|
};
|
||||||
@ -296,7 +265,6 @@
|
|||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
EA8379222F105F2600077F87 /* BusinessCard */,
|
EA8379222F105F2600077F87 /* BusinessCard */,
|
||||||
D007169724A44109B518B9E6 /* BusinessCardWatch */,
|
|
||||||
EA83792F2F105F2800077F87 /* BusinessCardTests */,
|
EA83792F2F105F2800077F87 /* BusinessCardTests */,
|
||||||
EA8379392F105F2800077F87 /* BusinessCardUITests */,
|
EA8379392F105F2800077F87 /* BusinessCardUITests */,
|
||||||
EA837F972F11B16400077F87 /* BusinessCardWatch Watch App */,
|
EA837F972F11B16400077F87 /* BusinessCardWatch Watch App */,
|
||||||
@ -305,13 +273,6 @@
|
|||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
9F6436BCE5F34967B6A4509D /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
EA8379212F105F2600077F87 /* Resources */ = {
|
EA8379212F105F2600077F87 /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@ -343,13 +304,6 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
7D1EBA94A23F41D5A441C5E4 /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
EA83791F2F105F2600077F87 /* Sources */ = {
|
EA83791F2F105F2600077F87 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@ -394,56 +348,6 @@
|
|||||||
/* End PBXTargetDependency section */
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
2AA803F1BF6442BEBBEA0D74 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
CODE_SIGN_ENTITLEMENTS = BusinessCardWatch/BusinessCardWatch.entitlements;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
|
||||||
ENABLE_PREVIEWS = YES;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCardWatch;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SDKROOT = watchos;
|
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
|
||||||
SUPPORTED_PLATFORMS = watchos;
|
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
|
||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
|
||||||
SWIFT_VERSION = 6.2;
|
|
||||||
TARGETED_DEVICE_FAMILY = 4;
|
|
||||||
WATCHOS_DEPLOYMENT_TARGET = 12.0;
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
B9B3B52E9CBF4C0BA6813348 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
CODE_SIGN_ENTITLEMENTS = BusinessCardWatch/BusinessCardWatch.entitlements;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
|
||||||
ENABLE_PREVIEWS = YES;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCardWatch;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SDKROOT = watchos;
|
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
|
||||||
SUPPORTED_PLATFORMS = watchos;
|
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
|
||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
|
||||||
SWIFT_VERSION = 6.2;
|
|
||||||
TARGETED_DEVICE_FAMILY = 4;
|
|
||||||
WATCHOS_DEPLOYMENT_TARGET = 12.0;
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
EA8379422F105F2800077F87 /* Debug */ = {
|
EA8379422F105F2800077F87 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@ -726,6 +630,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = "BusinessCardWatch Watch App/BusinessCardWatch.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||||
@ -733,13 +638,13 @@
|
|||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = BusinessCardWatch;
|
INFOPLIST_KEY_CFBundleDisplayName = BusinessCardWatch;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "";
|
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.mbrucedogs.BusinessCard;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = .watchkitapp;
|
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCard.watchkitapp;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = watchos;
|
SDKROOT = watchos;
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
@ -759,6 +664,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = "BusinessCardWatch Watch App/BusinessCardWatch.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||||
@ -766,13 +672,13 @@
|
|||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = BusinessCardWatch;
|
INFOPLIST_KEY_CFBundleDisplayName = BusinessCardWatch;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "";
|
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.mbrucedogs.BusinessCard;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = .watchkitapp;
|
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCard.watchkitapp;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = watchos;
|
SDKROOT = watchos;
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
@ -790,15 +696,6 @@
|
|||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
3873468A4B2043BDAA689772 /* Build configuration list for PBXNativeTarget "BusinessCardWatch" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
2AA803F1BF6442BEBBEA0D74 /* Debug */,
|
|
||||||
B9B3B52E9CBF4C0BA6813348 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
EA83791E2F105F2600077F87 /* Build configuration list for PBXProject "BusinessCard" */ = {
|
EA83791E2F105F2600077F87 /* Build configuration list for PBXProject "BusinessCard" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<array/>
|
||||||
|
</plist>
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Bucket
|
||||||
|
uuid = "6FB169DC-E619-40A8-968F-910EF3CF4FA4"
|
||||||
|
type = "1"
|
||||||
|
version = "2.0">
|
||||||
|
</Bucket>
|
||||||
@ -7,17 +7,12 @@
|
|||||||
<key>BusinessCard.xcscheme_^#shared#^_</key>
|
<key>BusinessCard.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>1</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>BusinessCardWatch Watch App.xcscheme_^#shared#^_</key>
|
<key>BusinessCardWatch Watch App.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>3</integer>
|
<integer>1</integer>
|
||||||
</dict>
|
|
||||||
<key>BusinessCardWatch.xcscheme_^#shared#^_</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>2</integer>
|
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@ -65,6 +65,9 @@ struct BusinessCardApp: App {
|
|||||||
self.modelContainer = container
|
self.modelContainer = container
|
||||||
let context = container.mainContext
|
let context = container.mainContext
|
||||||
self._appState = State(initialValue: AppState(modelContext: context))
|
self._appState = State(initialValue: AppState(modelContext: context))
|
||||||
|
|
||||||
|
// Activate WatchConnectivity session
|
||||||
|
_ = WatchConnectivityService.shared
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
|
|||||||
258
BusinessCard/Services/WatchConnectivityService.swift
Normal file
258
BusinessCard/Services/WatchConnectivityService.swift
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
import Foundation
|
||||||
|
import WatchConnectivity
|
||||||
|
import CoreImage
|
||||||
|
import CoreImage.CIFilterBuiltins
|
||||||
|
import UIKit
|
||||||
|
import Bedrock
|
||||||
|
|
||||||
|
/// Manages WatchConnectivity session and sends card data to Apple Watch
|
||||||
|
@MainActor
|
||||||
|
final class WatchConnectivityService: NSObject {
|
||||||
|
static let shared = WatchConnectivityService()
|
||||||
|
|
||||||
|
private var session: WCSession?
|
||||||
|
private var isActivated = false
|
||||||
|
private var pendingCards: [BusinessCard]?
|
||||||
|
|
||||||
|
private override init() {
|
||||||
|
super.init()
|
||||||
|
if WCSession.isSupported() {
|
||||||
|
session = WCSession.default
|
||||||
|
session?.delegate = self
|
||||||
|
session?.activate()
|
||||||
|
Design.debugLog("WatchConnectivity: Session activating...")
|
||||||
|
} else {
|
||||||
|
Design.debugLog("WatchConnectivity: Not supported on this device")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Syncs the given cards to the paired Apple Watch
|
||||||
|
func syncCards(_ cards: [BusinessCard]) {
|
||||||
|
// If not yet activated, store cards to send after activation
|
||||||
|
guard isActivated else {
|
||||||
|
Design.debugLog("WatchConnectivity: Session not ready, queuing \(cards.count) cards")
|
||||||
|
pendingCards = cards
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let session = session else {
|
||||||
|
Design.debugLog("WatchConnectivity: No session available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// In DEBUG, isWatchAppInstalled may be false even when running from Xcode
|
||||||
|
// So we only require isPaired and isReachable or just try to send anyway
|
||||||
|
#if DEBUG
|
||||||
|
guard session.isPaired else {
|
||||||
|
Design.debugLog("WatchConnectivity: Watch not paired")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Design.debugLog("WatchConnectivity: DEBUG mode - paired: \(session.isPaired), installed: \(session.isWatchAppInstalled), reachable: \(session.isReachable)")
|
||||||
|
#else
|
||||||
|
guard session.isPaired, session.isWatchAppInstalled else {
|
||||||
|
Design.debugLog("WatchConnectivity: Watch not available (paired: \(session.isPaired), installed: \(session.isWatchAppInstalled))")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
sendCardsToWatch(cards)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendCardsToWatch(_ cards: [BusinessCard]) {
|
||||||
|
Design.debugLog("WatchConnectivity: Syncing \(cards.count) cards to Watch")
|
||||||
|
|
||||||
|
let syncableCards = cards.map { card in
|
||||||
|
createSyncableCard(from: card)
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let encoded = try JSONEncoder().encode(syncableCards)
|
||||||
|
let userInfo: [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")
|
||||||
|
} catch {
|
||||||
|
Design.debugLog("WatchConnectivity: ERROR - Failed to encode: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func handleActivation() {
|
||||||
|
isActivated = true
|
||||||
|
|
||||||
|
// Send any pending cards that were queued before activation
|
||||||
|
if let cards = pendingCards {
|
||||||
|
Design.debugLog("WatchConnectivity: Sending \(cards.count) queued cards after activation")
|
||||||
|
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 ?? ""
|
||||||
|
let phone = card.firstContactField(ofType: "phone")?.value ?? ""
|
||||||
|
let website = card.firstContactField(ofType: "website")?.value ?? ""
|
||||||
|
let linkedIn = card.firstContactField(ofType: "linkedIn")?.value ?? ""
|
||||||
|
let twitter = card.firstContactField(ofType: "twitter")?.value ?? ""
|
||||||
|
let instagram = card.firstContactField(ofType: "instagram")?.value ?? ""
|
||||||
|
|
||||||
|
// Format address for display (decode from stored JSON/legacy format)
|
||||||
|
let addressValue = card.firstContactField(ofType: "address")?.value ?? ""
|
||||||
|
let location = PostalAddress.decode(from: addressValue)?.singleLineString ?? addressValue
|
||||||
|
|
||||||
|
// Build vCard payload for QR code generation
|
||||||
|
let vCardPayload = buildVCardPayload(
|
||||||
|
displayName: card.displayName,
|
||||||
|
company: card.company,
|
||||||
|
role: card.role,
|
||||||
|
phone: phone,
|
||||||
|
email: email,
|
||||||
|
website: website,
|
||||||
|
location: location,
|
||||||
|
bio: card.bio,
|
||||||
|
linkedIn: linkedIn,
|
||||||
|
twitter: twitter,
|
||||||
|
instagram: instagram
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generate QR code image data on iOS (CoreImage not available on watchOS)
|
||||||
|
let qrImageData = generateQRCodePNGData(from: vCardPayload)
|
||||||
|
|
||||||
|
return SyncableCard(
|
||||||
|
id: card.id,
|
||||||
|
displayName: card.displayName,
|
||||||
|
role: card.role,
|
||||||
|
company: card.company,
|
||||||
|
email: email,
|
||||||
|
phone: phone,
|
||||||
|
website: website,
|
||||||
|
location: location,
|
||||||
|
isDefault: card.isDefault,
|
||||||
|
pronouns: card.pronouns,
|
||||||
|
bio: card.bio,
|
||||||
|
linkedIn: linkedIn,
|
||||||
|
twitter: twitter,
|
||||||
|
instagram: instagram,
|
||||||
|
qrCodeImageData: qrImageData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func buildVCardPayload(
|
||||||
|
displayName: String,
|
||||||
|
company: String,
|
||||||
|
role: String,
|
||||||
|
phone: String,
|
||||||
|
email: String,
|
||||||
|
website: String,
|
||||||
|
location: String,
|
||||||
|
bio: String,
|
||||||
|
linkedIn: String,
|
||||||
|
twitter: String,
|
||||||
|
instagram: String
|
||||||
|
) -> String {
|
||||||
|
var lines = [
|
||||||
|
"BEGIN:VCARD",
|
||||||
|
"VERSION:3.0",
|
||||||
|
"FN:\(displayName)",
|
||||||
|
"ORG:\(company)",
|
||||||
|
"TITLE:\(role)"
|
||||||
|
]
|
||||||
|
|
||||||
|
if !phone.isEmpty {
|
||||||
|
lines.append("TEL;TYPE=work:\(phone)")
|
||||||
|
}
|
||||||
|
if !email.isEmpty {
|
||||||
|
lines.append("EMAIL;TYPE=work:\(email)")
|
||||||
|
}
|
||||||
|
if !website.isEmpty {
|
||||||
|
lines.append("URL:\(website)")
|
||||||
|
}
|
||||||
|
if !location.isEmpty {
|
||||||
|
lines.append("ADR;TYPE=work:;;\(location)")
|
||||||
|
}
|
||||||
|
if !bio.isEmpty {
|
||||||
|
lines.append("NOTE:\(bio)")
|
||||||
|
}
|
||||||
|
if !linkedIn.isEmpty {
|
||||||
|
lines.append("X-SOCIALPROFILE;TYPE=linkedin:\(linkedIn)")
|
||||||
|
}
|
||||||
|
if !twitter.isEmpty {
|
||||||
|
lines.append("X-SOCIALPROFILE;TYPE=twitter:\(twitter)")
|
||||||
|
}
|
||||||
|
if !instagram.isEmpty {
|
||||||
|
lines.append("X-SOCIALPROFILE;TYPE=instagram:\(instagram)")
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.append("END:VCARD")
|
||||||
|
return lines.joined(separator: "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func generateQRCodePNGData(from payload: String) -> Data? {
|
||||||
|
let context = CIContext()
|
||||||
|
let data = Data(payload.utf8)
|
||||||
|
let filter = CIFilter.qrCodeGenerator()
|
||||||
|
filter.setValue(data, forKey: "inputMessage")
|
||||||
|
filter.correctionLevel = "M"
|
||||||
|
|
||||||
|
guard let outputImage = filter.outputImage else { return nil }
|
||||||
|
|
||||||
|
// Scale up for better quality on watch display
|
||||||
|
let scaledImage = outputImage.transformed(by: CGAffineTransform(scaleX: 10, y: 10))
|
||||||
|
|
||||||
|
guard let cgImage = context.createCGImage(scaledImage, from: scaledImage.extent) else { return nil }
|
||||||
|
|
||||||
|
// Convert to PNG data for syncing
|
||||||
|
let uiImage = UIImage(cgImage: cgImage)
|
||||||
|
return uiImage.pngData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - WCSessionDelegate
|
||||||
|
|
||||||
|
extension WatchConnectivityService: WCSessionDelegate {
|
||||||
|
nonisolated func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
|
||||||
|
Task { @MainActor in
|
||||||
|
if let error = error {
|
||||||
|
Design.debugLog("WatchConnectivity: Activation failed: \(error)")
|
||||||
|
} else {
|
||||||
|
Design.debugLog("WatchConnectivity: Activated with state: \(activationState.rawValue)")
|
||||||
|
WatchConnectivityService.shared.handleActivation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated func sessionDidBecomeInactive(_ session: WCSession) {
|
||||||
|
Task { @MainActor in
|
||||||
|
Design.debugLog("WatchConnectivity: Session became inactive")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated func sessionDidDeactivate(_ session: WCSession) {
|
||||||
|
Task { @MainActor in
|
||||||
|
Design.debugLog("WatchConnectivity: Session deactivated, reactivating...")
|
||||||
|
WCSession.default.activate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A simplified card structure that can be shared between iOS and watchOS
|
||||||
|
struct SyncableCard: Codable, Identifiable {
|
||||||
|
let id: UUID
|
||||||
|
var displayName: String
|
||||||
|
var role: String
|
||||||
|
var company: String
|
||||||
|
var email: String
|
||||||
|
var phone: String
|
||||||
|
var website: String
|
||||||
|
var location: String
|
||||||
|
var isDefault: Bool
|
||||||
|
var pronouns: String
|
||||||
|
var bio: String
|
||||||
|
var linkedIn: String
|
||||||
|
var twitter: String
|
||||||
|
var instagram: String
|
||||||
|
/// Pre-generated QR code PNG data (CoreImage not available on watchOS)
|
||||||
|
var qrCodeImageData: Data?
|
||||||
|
}
|
||||||
@ -1,201 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import CoreImage
|
|
||||||
import CoreImage.CIFilterBuiltins
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
/// Syncs card data to watchOS via shared App Group UserDefaults
|
|
||||||
struct WatchSyncService {
|
|
||||||
private static let appGroupID = "group.com.mbrucedogs.BusinessCard"
|
|
||||||
private static let cardsKey = "SyncedCards"
|
|
||||||
|
|
||||||
private static var sharedDefaults: UserDefaults? {
|
|
||||||
UserDefaults(suiteName: appGroupID)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Syncs the given cards to the shared App Group for watchOS to read
|
|
||||||
@MainActor
|
|
||||||
static func syncCards(_ cards: [BusinessCard]) {
|
|
||||||
guard let defaults = sharedDefaults else { return }
|
|
||||||
|
|
||||||
let syncableCards = cards.map { card in
|
|
||||||
// Get first email, phone, website, location from contact fields
|
|
||||||
let email = card.firstContactField(ofType: "email")?.value ?? ""
|
|
||||||
let phone = card.firstContactField(ofType: "phone")?.value ?? ""
|
|
||||||
let website = card.firstContactField(ofType: "website")?.value ?? ""
|
|
||||||
let linkedIn = card.firstContactField(ofType: "linkedIn")?.value ?? ""
|
|
||||||
let twitter = card.firstContactField(ofType: "twitter")?.value ?? ""
|
|
||||||
let instagram = card.firstContactField(ofType: "instagram")?.value ?? ""
|
|
||||||
|
|
||||||
// Format address for display (decode from stored JSON/legacy format)
|
|
||||||
let addressValue = card.firstContactField(ofType: "address")?.value ?? ""
|
|
||||||
let location = PostalAddress.decode(from: addressValue)?.singleLineString ?? addressValue
|
|
||||||
|
|
||||||
// Build vCard payload for QR code generation
|
|
||||||
let vCardPayload = buildVCardPayload(
|
|
||||||
displayName: card.displayName,
|
|
||||||
company: card.company,
|
|
||||||
role: card.role,
|
|
||||||
phone: phone,
|
|
||||||
email: email,
|
|
||||||
website: website,
|
|
||||||
location: location,
|
|
||||||
bio: card.bio,
|
|
||||||
linkedIn: linkedIn,
|
|
||||||
twitter: twitter,
|
|
||||||
instagram: instagram
|
|
||||||
)
|
|
||||||
|
|
||||||
// Generate QR code image data on iOS (CoreImage not available on watchOS)
|
|
||||||
let qrImageData = generateQRCodePNGData(from: vCardPayload)
|
|
||||||
|
|
||||||
return SyncableCard(
|
|
||||||
id: card.id,
|
|
||||||
displayName: card.displayName,
|
|
||||||
role: card.role,
|
|
||||||
company: card.company,
|
|
||||||
email: email,
|
|
||||||
phone: phone,
|
|
||||||
website: website,
|
|
||||||
location: location,
|
|
||||||
isDefault: card.isDefault,
|
|
||||||
pronouns: card.pronouns,
|
|
||||||
bio: card.bio,
|
|
||||||
linkedIn: linkedIn,
|
|
||||||
twitter: twitter,
|
|
||||||
instagram: instagram,
|
|
||||||
qrCodeImageData: qrImageData
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let encoded = try? JSONEncoder().encode(syncableCards) {
|
|
||||||
defaults.set(encoded, forKey: cardsKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func buildVCardPayload(
|
|
||||||
displayName: String,
|
|
||||||
company: String,
|
|
||||||
role: String,
|
|
||||||
phone: String,
|
|
||||||
email: String,
|
|
||||||
website: String,
|
|
||||||
location: String,
|
|
||||||
bio: String,
|
|
||||||
linkedIn: String,
|
|
||||||
twitter: String,
|
|
||||||
instagram: String
|
|
||||||
) -> String {
|
|
||||||
var lines = [
|
|
||||||
"BEGIN:VCARD",
|
|
||||||
"VERSION:3.0",
|
|
||||||
"FN:\(displayName)",
|
|
||||||
"ORG:\(company)",
|
|
||||||
"TITLE:\(role)"
|
|
||||||
]
|
|
||||||
|
|
||||||
if !phone.isEmpty {
|
|
||||||
lines.append("TEL;TYPE=work:\(phone)")
|
|
||||||
}
|
|
||||||
if !email.isEmpty {
|
|
||||||
lines.append("EMAIL;TYPE=work:\(email)")
|
|
||||||
}
|
|
||||||
if !website.isEmpty {
|
|
||||||
lines.append("URL:\(website)")
|
|
||||||
}
|
|
||||||
if !location.isEmpty {
|
|
||||||
lines.append("ADR;TYPE=work:;;\(location)")
|
|
||||||
}
|
|
||||||
if !bio.isEmpty {
|
|
||||||
lines.append("NOTE:\(bio)")
|
|
||||||
}
|
|
||||||
if !linkedIn.isEmpty {
|
|
||||||
lines.append("X-SOCIALPROFILE;TYPE=linkedin:\(linkedIn)")
|
|
||||||
}
|
|
||||||
if !twitter.isEmpty {
|
|
||||||
lines.append("X-SOCIALPROFILE;TYPE=twitter:\(twitter)")
|
|
||||||
}
|
|
||||||
if !instagram.isEmpty {
|
|
||||||
lines.append("X-SOCIALPROFILE;TYPE=instagram:\(instagram)")
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.append("END:VCARD")
|
|
||||||
return lines.joined(separator: "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func generateQRCodePNGData(from payload: String) -> Data? {
|
|
||||||
let context = CIContext()
|
|
||||||
let data = Data(payload.utf8)
|
|
||||||
let filter = CIFilter.qrCodeGenerator()
|
|
||||||
filter.setValue(data, forKey: "inputMessage")
|
|
||||||
filter.correctionLevel = "M"
|
|
||||||
|
|
||||||
guard let outputImage = filter.outputImage else { return nil }
|
|
||||||
|
|
||||||
// Scale up for better quality on watch display
|
|
||||||
let scaledImage = outputImage.transformed(by: CGAffineTransform(scaleX: 10, y: 10))
|
|
||||||
|
|
||||||
guard let cgImage = context.createCGImage(scaledImage, from: scaledImage.extent) else { return nil }
|
|
||||||
|
|
||||||
// Convert to PNG data for syncing
|
|
||||||
let uiImage = UIImage(cgImage: cgImage)
|
|
||||||
return uiImage.pngData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A simplified card structure that can be shared between iOS and watchOS
|
|
||||||
struct SyncableCard: Codable, Identifiable {
|
|
||||||
let id: UUID
|
|
||||||
var displayName: String
|
|
||||||
var role: String
|
|
||||||
var company: String
|
|
||||||
var email: String
|
|
||||||
var phone: String
|
|
||||||
var website: String
|
|
||||||
var location: String
|
|
||||||
var isDefault: Bool
|
|
||||||
var pronouns: String
|
|
||||||
var bio: String
|
|
||||||
var linkedIn: String
|
|
||||||
var twitter: String
|
|
||||||
var instagram: String
|
|
||||||
/// Pre-generated QR code PNG data (CoreImage not available on watchOS)
|
|
||||||
var qrCodeImageData: Data?
|
|
||||||
|
|
||||||
var vCardPayload: String {
|
|
||||||
var lines = [
|
|
||||||
"BEGIN:VCARD",
|
|
||||||
"VERSION:3.0",
|
|
||||||
"FN:\(displayName)",
|
|
||||||
"ORG:\(company)",
|
|
||||||
"TITLE:\(role)"
|
|
||||||
]
|
|
||||||
|
|
||||||
if !phone.isEmpty {
|
|
||||||
lines.append("TEL;TYPE=work:\(phone)")
|
|
||||||
}
|
|
||||||
if !email.isEmpty {
|
|
||||||
lines.append("EMAIL;TYPE=work:\(email)")
|
|
||||||
}
|
|
||||||
if !website.isEmpty {
|
|
||||||
lines.append("URL:\(website)")
|
|
||||||
}
|
|
||||||
if !location.isEmpty {
|
|
||||||
lines.append("ADR;TYPE=work:;;\(location)")
|
|
||||||
}
|
|
||||||
if !bio.isEmpty {
|
|
||||||
lines.append("NOTE:\(bio)")
|
|
||||||
}
|
|
||||||
if !linkedIn.isEmpty {
|
|
||||||
lines.append("X-SOCIALPROFILE;TYPE=linkedin:\(linkedIn)")
|
|
||||||
}
|
|
||||||
if !twitter.isEmpty {
|
|
||||||
lines.append("X-SOCIALPROFILE;TYPE=twitter:\(twitter)")
|
|
||||||
}
|
|
||||||
if !instagram.isEmpty {
|
|
||||||
lines.append("X-SOCIALPROFILE;TYPE=instagram:\(instagram)")
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.append("END:VCARD")
|
|
||||||
return lines.joined(separator: "\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Observation
|
import Observation
|
||||||
import SwiftData
|
import SwiftData
|
||||||
|
import Bedrock
|
||||||
|
|
||||||
@Observable
|
@Observable
|
||||||
@MainActor
|
@MainActor
|
||||||
@ -41,6 +42,7 @@ final class CardStore: BusinessCardProviding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateCard(_ card: BusinessCard) {
|
func updateCard(_ card: BusinessCard) {
|
||||||
|
Design.debugLog("CardStore: updateCard called for: \(card.displayName)")
|
||||||
card.updatedAt = .now
|
card.updatedAt = .now
|
||||||
saveContext()
|
saveContext()
|
||||||
fetchCards()
|
fetchCards()
|
||||||
@ -90,6 +92,7 @@ final class CardStore: BusinessCardProviding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func syncToWatch() {
|
private func syncToWatch() {
|
||||||
WatchSyncService.syncCards(cards)
|
Design.debugLog("CardStore: syncToWatch called with \(cards.count) cards")
|
||||||
|
WatchConnectivityService.shared.syncCards(cards)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict/>
|
||||||
<key>com.apple.security.application-groups</key>
|
|
||||||
<array>
|
|
||||||
<string>group.com.mbrucedogs.BusinessCard</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -4,6 +4,16 @@ import SwiftUI
|
|||||||
struct BusinessCardWatchApp: App {
|
struct BusinessCardWatchApp: App {
|
||||||
@State private var cardStore = WatchCardStore()
|
@State private var cardStore = WatchCardStore()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Wire up WatchConnectivity to receive cards from iPhone
|
||||||
|
let store = cardStore
|
||||||
|
WatchConnectivityService.shared.onCardsReceived = { cards in
|
||||||
|
Task { @MainActor in
|
||||||
|
store.updateCards(cards)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
WatchContentView()
|
WatchContentView()
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum WatchDesign {
|
enum WatchDesign {
|
||||||
|
/// Debug logging for watch app - only prints in DEBUG builds
|
||||||
|
static func debugLog(_ message: String) {
|
||||||
|
#if DEBUG
|
||||||
|
print("[WatchApp] \(message)")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
enum Spacing {
|
enum Spacing {
|
||||||
static let small: CGFloat = 6
|
static let small: CGFloat = 6
|
||||||
static let medium: CGFloat = 10
|
static let medium: CGFloat = 10
|
||||||
|
|||||||
@ -0,0 +1,118 @@
|
|||||||
|
import Foundation
|
||||||
|
import WatchConnectivity
|
||||||
|
|
||||||
|
/// Manages WatchConnectivity session and receives card data from iPhone
|
||||||
|
@MainActor
|
||||||
|
final class WatchConnectivityService: NSObject {
|
||||||
|
static let shared = WatchConnectivityService()
|
||||||
|
|
||||||
|
/// Callback when cards are received from iPhone
|
||||||
|
var onCardsReceived: (([WatchCard]) -> Void)?
|
||||||
|
|
||||||
|
private var session: WCSession?
|
||||||
|
|
||||||
|
private override init() {
|
||||||
|
super.init()
|
||||||
|
if WCSession.isSupported() {
|
||||||
|
session = WCSession.default
|
||||||
|
session?.delegate = self
|
||||||
|
session?.activate()
|
||||||
|
WatchDesign.debugLog("WatchConnectivity: Session activating...")
|
||||||
|
} else {
|
||||||
|
WatchDesign.debugLog("WatchConnectivity: Not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check for any existing application context on launch
|
||||||
|
func checkForExistingContext() {
|
||||||
|
guard let session = session else { return }
|
||||||
|
|
||||||
|
let context = session.receivedApplicationContext
|
||||||
|
if !context.isEmpty {
|
||||||
|
WatchDesign.debugLog("WatchConnectivity: Found existing context on launch")
|
||||||
|
processReceivedContext(context)
|
||||||
|
} else {
|
||||||
|
WatchDesign.debugLog("WatchConnectivity: No existing context found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func processReceivedContext(_ context: [String: Any]) {
|
||||||
|
guard let cardsData = context["cards"] as? Data else {
|
||||||
|
WatchDesign.debugLog("WatchConnectivity: No cards data in context")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
WatchDesign.debugLog("WatchConnectivity: Received \(cardsData.count) bytes")
|
||||||
|
|
||||||
|
do {
|
||||||
|
let syncableCards = try JSONDecoder().decode([SyncableCard].self, from: cardsData)
|
||||||
|
let watchCards = syncableCards.map { syncable in
|
||||||
|
WatchCard(
|
||||||
|
id: syncable.id,
|
||||||
|
displayName: syncable.displayName,
|
||||||
|
role: syncable.role,
|
||||||
|
company: syncable.company,
|
||||||
|
email: syncable.email,
|
||||||
|
phone: syncable.phone,
|
||||||
|
website: syncable.website,
|
||||||
|
location: syncable.location,
|
||||||
|
isDefault: syncable.isDefault,
|
||||||
|
qrCodeImageData: syncable.qrCodeImageData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
WatchDesign.debugLog("WatchConnectivity: Decoded \(watchCards.count) cards")
|
||||||
|
onCardsReceived?(watchCards)
|
||||||
|
} catch {
|
||||||
|
WatchDesign.debugLog("WatchConnectivity: ERROR - Failed to decode: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - WCSessionDelegate
|
||||||
|
|
||||||
|
extension WatchConnectivityService: WCSessionDelegate {
|
||||||
|
nonisolated func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
|
||||||
|
Task { @MainActor in
|
||||||
|
if let error = error {
|
||||||
|
WatchDesign.debugLog("WatchConnectivity: Activation failed: \(error)")
|
||||||
|
} else {
|
||||||
|
WatchDesign.debugLog("WatchConnectivity: Activated with state: \(activationState.rawValue)")
|
||||||
|
// Check for existing context after activation
|
||||||
|
checkForExistingContext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
|
||||||
|
Task { @MainActor in
|
||||||
|
WatchDesign.debugLog("WatchConnectivity: Received application context update")
|
||||||
|
processReceivedContext(applicationContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
|
||||||
|
Task { @MainActor in
|
||||||
|
WatchDesign.debugLog("WatchConnectivity: Received userInfo transfer")
|
||||||
|
processReceivedContext(userInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Syncable card structure matching iOS side (for decoding)
|
||||||
|
private struct SyncableCard: Codable, Identifiable {
|
||||||
|
let id: UUID
|
||||||
|
var displayName: String
|
||||||
|
var role: String
|
||||||
|
var company: String
|
||||||
|
var email: String
|
||||||
|
var phone: String
|
||||||
|
var website: String
|
||||||
|
var location: String
|
||||||
|
var isDefault: Bool
|
||||||
|
var pronouns: String
|
||||||
|
var bio: String
|
||||||
|
var linkedIn: String
|
||||||
|
var twitter: String
|
||||||
|
var instagram: String
|
||||||
|
var qrCodeImageData: Data?
|
||||||
|
}
|
||||||
@ -4,8 +4,6 @@ import Observation
|
|||||||
@Observable
|
@Observable
|
||||||
@MainActor
|
@MainActor
|
||||||
final class WatchCardStore {
|
final class WatchCardStore {
|
||||||
private static let appGroupID = "group.com.mbrucedogs.BusinessCard"
|
|
||||||
private static let cardsKey = "SyncedCards"
|
|
||||||
private static let defaultCardIDKey = "WatchDefaultCardID"
|
private static let defaultCardIDKey = "WatchDefaultCardID"
|
||||||
|
|
||||||
private(set) var cards: [WatchCard] = []
|
private(set) var cards: [WatchCard] = []
|
||||||
@ -15,28 +13,27 @@ final class WatchCardStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var sharedDefaults: UserDefaults? {
|
|
||||||
UserDefaults(suiteName: Self.appGroupID)
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
loadCards()
|
|
||||||
loadDefaultID()
|
loadDefaultID()
|
||||||
|
WatchDesign.debugLog("WatchCardStore: Initialized, waiting for cards from iPhone")
|
||||||
}
|
}
|
||||||
|
|
||||||
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 }
|
||||||
return cards.first(where: { $0.id == defaultCardID }) ?? cards.first(where: { $0.isDefault }) ?? cards.first
|
return cards.first(where: { $0.id == defaultCardID }) ?? cards.first(where: { $0.isDefault }) ?? cards.first
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadCards() {
|
/// Called by WatchConnectivityService when cards are received from iPhone
|
||||||
guard let defaults = sharedDefaults,
|
func updateCards(_ newCards: [WatchCard]) {
|
||||||
let data = defaults.data(forKey: Self.cardsKey),
|
WatchDesign.debugLog("WatchCardStore: Received \(newCards.count) cards from iPhone")
|
||||||
let decoded = try? JSONDecoder().decode([WatchCard].self, from: data) else {
|
cards = newCards
|
||||||
cards = []
|
|
||||||
return
|
// Update default card ID if current selection is no longer valid
|
||||||
|
if let currentDefault = defaultCardID, !cards.contains(where: { $0.id == currentDefault }) {
|
||||||
|
defaultCardID = cards.first(where: { $0.isDefault })?.id ?? cards.first?.id
|
||||||
|
} else if defaultCardID == nil {
|
||||||
|
defaultCardID = cards.first(where: { $0.isDefault })?.id ?? cards.first?.id
|
||||||
}
|
}
|
||||||
cards = decoded
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setDefault(_ card: WatchCard) {
|
func setDefault(_ card: WatchCard) {
|
||||||
@ -49,10 +46,8 @@ final class WatchCardStore {
|
|||||||
|
|
||||||
private func loadDefaultID() {
|
private func loadDefaultID() {
|
||||||
let storedValue = UserDefaults.standard.string(forKey: Self.defaultCardIDKey) ?? ""
|
let storedValue = UserDefaults.standard.string(forKey: Self.defaultCardIDKey) ?? ""
|
||||||
if let id = UUID(uuidString: storedValue), cards.contains(where: { $0.id == id }) {
|
if let id = UUID(uuidString: storedValue) {
|
||||||
defaultCardID = id
|
defaultCardID = id
|
||||||
} else {
|
|
||||||
defaultCardID = cards.first(where: { $0.isDefault })?.id ?? cards.first?.id
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,9 +19,6 @@ struct WatchContentView: View {
|
|||||||
.padding(WatchDesign.Spacing.medium)
|
.padding(WatchDesign.Spacing.medium)
|
||||||
}
|
}
|
||||||
.background(Color.WatchPalette.background)
|
.background(Color.WatchPalette.background)
|
||||||
.onAppear {
|
|
||||||
cardStore.loadCards()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,8 +71,8 @@ private struct WatchQRCodeCardView: View {
|
|||||||
.foregroundStyle(Color.WatchPalette.muted)
|
.foregroundStyle(Color.WatchPalette.muted)
|
||||||
}
|
}
|
||||||
.frame(width: WatchDesign.Size.qrSize, height: WatchDesign.Size.qrSize)
|
.frame(width: WatchDesign.Size.qrSize, height: WatchDesign.Size.qrSize)
|
||||||
.background(Color.WatchPalette.card)
|
.background(Color.WatchPalette.card)
|
||||||
.clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.large))
|
.clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.large))
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(card.displayName)
|
Text(card.displayName)
|
||||||
|
|||||||
10
README.md
10
README.md
@ -98,7 +98,7 @@ Each field has:
|
|||||||
|
|
||||||
- Shows the default card QR code
|
- Shows the default card QR code
|
||||||
- Pick which card is the default on watch
|
- Pick which card is the default on watch
|
||||||
- **Syncs with iPhone** via App Groups
|
- **Syncs with iPhone** via WatchConnectivity
|
||||||
|
|
||||||
## Data Sync
|
## Data Sync
|
||||||
|
|
||||||
@ -108,9 +108,9 @@ Cards and contacts are stored using SwiftData with CloudKit sync enabled. Your d
|
|||||||
|
|
||||||
### iPhone to Watch Sync
|
### iPhone to Watch Sync
|
||||||
|
|
||||||
The iPhone app syncs card data to the paired Apple Watch via App Groups. When you create, edit, or delete cards on your iPhone, the changes appear on your watch.
|
The iPhone app syncs card data to the paired Apple Watch via WatchConnectivity framework. When you create, edit, or delete cards on your iPhone, the changes are pushed to your watch automatically.
|
||||||
|
|
||||||
**Note**: The watch reads data from the iPhone. To update cards on the watch, make changes on the iPhone first.
|
**Note**: The watch receives data from the iPhone. To update cards on the watch, make changes on the iPhone first. QR codes are pre-generated on iPhone since CoreImage is not available on watchOS.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@ -170,13 +170,13 @@ BusinessCardTests/ # Unit tests
|
|||||||
**iOS Target:**
|
**iOS Target:**
|
||||||
|
|
||||||
- iCloud (CloudKit enabled)
|
- iCloud (CloudKit enabled)
|
||||||
- App Groups (`group.com.mbrucedogs.BusinessCard`)
|
- App Groups (`group.com.mbrucedogs.BusinessCard`) - for future widget access
|
||||||
- Background Modes (Remote notifications)
|
- Background Modes (Remote notifications)
|
||||||
- Camera (for QR code scanning)
|
- Camera (for QR code scanning)
|
||||||
|
|
||||||
**watchOS Target:**
|
**watchOS Target:**
|
||||||
|
|
||||||
- App Groups (`group.com.mbrucedogs.BusinessCard`)
|
- WatchConnectivity (for receiving cards from iPhone)
|
||||||
|
|
||||||
### CloudKit Container
|
### CloudKit Container
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user