Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
8596a699c1
commit
3007805011
@ -32,6 +32,7 @@
|
|||||||
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; };
|
||||||
|
EA837F982F11B16400077F87 /* BusinessCardWatch Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "BusinessCardWatch Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
@ -45,11 +46,6 @@
|
|||||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
05CFDAD65474442D8E3E309E /* BusinessCardWatch */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
path = BusinessCardWatch;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
EA8379252F105F2600077F87 /* BusinessCard */ = {
|
EA8379252F105F2600077F87 /* BusinessCard */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
exceptions = (
|
||||||
@ -68,6 +64,11 @@
|
|||||||
path = BusinessCardUITests;
|
path = BusinessCardUITests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
EA837F992F11B16400077F87 /* BusinessCardWatch Watch App */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
path = "BusinessCardWatch Watch App";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -100,6 +101,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
EA837F952F11B16400077F87 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
@ -107,9 +115,9 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
EA8379252F105F2600077F87 /* BusinessCard */,
|
EA8379252F105F2600077F87 /* BusinessCard */,
|
||||||
05CFDAD65474442D8E3E309E /* BusinessCardWatch */,
|
|
||||||
EA8379332F105F2800077F87 /* BusinessCardTests */,
|
EA8379332F105F2800077F87 /* BusinessCardTests */,
|
||||||
EA83793D2F105F2800077F87 /* BusinessCardUITests */,
|
EA83793D2F105F2800077F87 /* BusinessCardUITests */,
|
||||||
|
EA837F992F11B16400077F87 /* BusinessCardWatch Watch App */,
|
||||||
EA8379242F105F2600077F87 /* Products */,
|
EA8379242F105F2600077F87 /* Products */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -121,6 +129,7 @@
|
|||||||
60186E73BC8040538616865B /* BusinessCardWatch.app */,
|
60186E73BC8040538616865B /* BusinessCardWatch.app */,
|
||||||
EA8379302F105F2800077F87 /* BusinessCardTests.xctest */,
|
EA8379302F105F2800077F87 /* BusinessCardTests.xctest */,
|
||||||
EA83793A2F105F2800077F87 /* BusinessCardUITests.xctest */,
|
EA83793A2F105F2800077F87 /* BusinessCardUITests.xctest */,
|
||||||
|
EA837F982F11B16400077F87 /* BusinessCardWatch Watch App.app */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -140,9 +149,6 @@
|
|||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
fileSystemSynchronizedGroups = (
|
|
||||||
05CFDAD65474442D8E3E309E /* BusinessCardWatch */,
|
|
||||||
);
|
|
||||||
name = BusinessCardWatch;
|
name = BusinessCardWatch;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
);
|
);
|
||||||
@ -219,6 +225,28 @@
|
|||||||
productReference = EA83793A2F105F2800077F87 /* BusinessCardUITests.xctest */;
|
productReference = EA83793A2F105F2800077F87 /* BusinessCardUITests.xctest */;
|
||||||
productType = "com.apple.product-type.bundle.ui-testing";
|
productType = "com.apple.product-type.bundle.ui-testing";
|
||||||
};
|
};
|
||||||
|
EA837F972F11B16400077F87 /* BusinessCardWatch Watch App */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = EA837FA02F11B16400077F87 /* Build configuration list for PBXNativeTarget "BusinessCardWatch Watch App" */;
|
||||||
|
buildPhases = (
|
||||||
|
EA837F942F11B16400077F87 /* Sources */,
|
||||||
|
EA837F952F11B16400077F87 /* Frameworks */,
|
||||||
|
EA837F962F11B16400077F87 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
EA837F992F11B16400077F87 /* BusinessCardWatch Watch App */,
|
||||||
|
);
|
||||||
|
name = "BusinessCardWatch Watch App";
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = "BusinessCardWatch Watch App";
|
||||||
|
productReference = EA837F982F11B16400077F87 /* BusinessCardWatch Watch App.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
@ -243,6 +271,9 @@
|
|||||||
CreatedOnToolsVersion = 26.0;
|
CreatedOnToolsVersion = 26.0;
|
||||||
TestTargetID = EA8379222F105F2600077F87;
|
TestTargetID = EA8379222F105F2600077F87;
|
||||||
};
|
};
|
||||||
|
EA837F972F11B16400077F87 = {
|
||||||
|
CreatedOnToolsVersion = 26.0;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
buildConfigurationList = EA83791E2F105F2600077F87 /* Build configuration list for PBXProject "BusinessCard" */;
|
buildConfigurationList = EA83791E2F105F2600077F87 /* Build configuration list for PBXProject "BusinessCard" */;
|
||||||
@ -268,6 +299,7 @@
|
|||||||
D007169724A44109B518B9E6 /* BusinessCardWatch */,
|
D007169724A44109B518B9E6 /* BusinessCardWatch */,
|
||||||
EA83792F2F105F2800077F87 /* BusinessCardTests */,
|
EA83792F2F105F2800077F87 /* BusinessCardTests */,
|
||||||
EA8379392F105F2800077F87 /* BusinessCardUITests */,
|
EA8379392F105F2800077F87 /* BusinessCardUITests */,
|
||||||
|
EA837F972F11B16400077F87 /* BusinessCardWatch Watch App */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@ -301,6 +333,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
EA837F962F11B16400077F87 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@ -332,6 +371,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
EA837F942F11B16400077F87 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
/* Begin PBXTargetDependency section */
|
||||||
@ -675,6 +721,72 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
EA837FA12F11B16400077F87 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||||
|
ENABLE_PREVIEWS = YES;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = BusinessCardWatch;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
|
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = .watchkitapp;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
EA837FA22F11B16400077F87 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||||
|
ENABLE_PREVIEWS = YES;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = BusinessCardWatch;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
|
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = .watchkitapp;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
@ -723,6 +835,15 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
EA837FA02F11B16400077F87 /* Build configuration list for PBXNativeTarget "BusinessCardWatch Watch App" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
EA837FA12F11B16400077F87 /* Debug */,
|
||||||
|
EA837FA22F11B16400077F87 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCLocalSwiftPackageReference section */
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
|
|||||||
@ -7,12 +7,17 @@
|
|||||||
<key>BusinessCard.xcscheme_^#shared#^_</key>
|
<key>BusinessCard.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>2</integer>
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
<key>BusinessCardWatch Watch App.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>3</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>BusinessCardWatch.xcscheme_^#shared#^_</key>
|
<key>BusinessCardWatch.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>1</integer>
|
<integer>2</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import CoreImage
|
||||||
|
import CoreImage.CIFilterBuiltins
|
||||||
|
import UIKit
|
||||||
|
|
||||||
/// Syncs card data to watchOS via shared App Group UserDefaults
|
/// Syncs card data to watchOS via shared App Group UserDefaults
|
||||||
struct WatchSyncService {
|
struct WatchSyncService {
|
||||||
@ -27,6 +30,24 @@ struct WatchSyncService {
|
|||||||
let addressValue = card.firstContactField(ofType: "address")?.value ?? ""
|
let addressValue = card.firstContactField(ofType: "address")?.value ?? ""
|
||||||
let location = PostalAddress.decode(from: addressValue)?.singleLineString ?? addressValue
|
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(
|
return SyncableCard(
|
||||||
id: card.id,
|
id: card.id,
|
||||||
displayName: card.displayName,
|
displayName: card.displayName,
|
||||||
@ -41,7 +62,8 @@ struct WatchSyncService {
|
|||||||
bio: card.bio,
|
bio: card.bio,
|
||||||
linkedIn: linkedIn,
|
linkedIn: linkedIn,
|
||||||
twitter: twitter,
|
twitter: twitter,
|
||||||
instagram: instagram
|
instagram: instagram,
|
||||||
|
qrCodeImageData: qrImageData
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,6 +71,75 @@ struct WatchSyncService {
|
|||||||
defaults.set(encoded, forKey: cardsKey)
|
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
|
/// A simplified card structure that can be shared between iOS and watchOS
|
||||||
@ -67,6 +158,8 @@ struct SyncableCard: Codable, Identifiable {
|
|||||||
var linkedIn: String
|
var linkedIn: String
|
||||||
var twitter: String
|
var twitter: String
|
||||||
var instagram: String
|
var instagram: String
|
||||||
|
/// Pre-generated QR code PNG data (CoreImage not available on watchOS)
|
||||||
|
var qrCodeImageData: Data?
|
||||||
|
|
||||||
var vCardPayload: String {
|
var vCardPayload: String {
|
||||||
var lines = [
|
var lines = [
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
/// A simplified card structure synced from the iOS app via App Group UserDefaults
|
/// A simplified card structure synced from the iOS app via App Group UserDefaults
|
||||||
struct WatchCard: Codable, Identifiable, Hashable {
|
struct WatchCard: Codable, Identifiable, Hashable {
|
||||||
@ -11,21 +12,14 @@ struct WatchCard: Codable, Identifiable, Hashable {
|
|||||||
var website: String
|
var website: String
|
||||||
var location: String
|
var location: String
|
||||||
var isDefault: Bool
|
var isDefault: Bool
|
||||||
|
/// Pre-generated QR code PNG data from iOS (CoreImage not available on watchOS)
|
||||||
|
var qrCodeImageData: Data?
|
||||||
|
|
||||||
var vCardPayload: String {
|
/// Returns a SwiftUI Image from the synced QR code data
|
||||||
let lines = [
|
var qrCodeImage: Image? {
|
||||||
"BEGIN:VCARD",
|
guard let data = qrCodeImageData,
|
||||||
"VERSION:3.0",
|
let uiImage = UIImage(data: data) else { return nil }
|
||||||
"FN:\(displayName)",
|
return Image(uiImage: uiImage)
|
||||||
"ORG:\(company)",
|
|
||||||
"TITLE:\(role)",
|
|
||||||
"TEL;TYPE=work:\(phone)",
|
|
||||||
"EMAIL;TYPE=work:\(email)",
|
|
||||||
"URL:\(website)",
|
|
||||||
"ADR;TYPE=work:;;\(location)",
|
|
||||||
"END:VCARD"
|
|
||||||
]
|
|
||||||
return lines.joined(separator: "\n")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +34,8 @@ extension WatchCard {
|
|||||||
phone: "+1 (214) 987-7810",
|
phone: "+1 (214) 987-7810",
|
||||||
website: "wrconstruction.co",
|
website: "wrconstruction.co",
|
||||||
location: "Dallas, TX",
|
location: "Dallas, TX",
|
||||||
isDefault: true
|
isDefault: true,
|
||||||
|
qrCodeImageData: nil
|
||||||
),
|
),
|
||||||
WatchCard(
|
WatchCard(
|
||||||
id: UUID(),
|
id: UUID(),
|
||||||
@ -51,7 +46,8 @@ extension WatchCard {
|
|||||||
phone: "+1 (312) 404-2211",
|
phone: "+1 (312) 404-2211",
|
||||||
website: "signal.studio",
|
website: "signal.studio",
|
||||||
location: "Chicago, IL",
|
location: "Chicago, IL",
|
||||||
isDefault: false
|
isDefault: false,
|
||||||
|
qrCodeImageData: nil
|
||||||
),
|
),
|
||||||
WatchCard(
|
WatchCard(
|
||||||
id: UUID(),
|
id: UUID(),
|
||||||
@ -62,7 +58,8 @@ extension WatchCard {
|
|||||||
phone: "+1 (646) 222-3300",
|
phone: "+1 (646) 222-3300",
|
||||||
website: "livesessions.fm",
|
website: "livesessions.fm",
|
||||||
location: "New York, NY",
|
location: "New York, NY",
|
||||||
isDefault: false
|
isDefault: false,
|
||||||
|
qrCodeImageData: nil
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -29,5 +29,6 @@
|
|||||||
"fr-CA" : { "stringUnit" : { "state" : "translated", "value" : "Sélectionnée" } }
|
"fr-CA" : { "stringUnit" : { "state" : "translated", "value" : "Sélectionnée" } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"version" : "1.1"
|
||||||
}
|
}
|
||||||
@ -2,13 +2,12 @@ import SwiftUI
|
|||||||
|
|
||||||
struct WatchContentView: View {
|
struct WatchContentView: View {
|
||||||
@Environment(WatchCardStore.self) private var cardStore
|
@Environment(WatchCardStore.self) private var cardStore
|
||||||
private let qrService = WatchQRCodeService()
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: WatchDesign.Spacing.large) {
|
VStack(spacing: WatchDesign.Spacing.large) {
|
||||||
if let card = cardStore.defaultCard {
|
if let card = cardStore.defaultCard {
|
||||||
WatchQRCodeCardView(card: card, qrService: qrService)
|
WatchQRCodeCardView(card: card)
|
||||||
} else if cardStore.cards.isEmpty {
|
} else if cardStore.cards.isEmpty {
|
||||||
WatchEmptyStateView()
|
WatchEmptyStateView()
|
||||||
}
|
}
|
||||||
@ -48,7 +47,6 @@ private struct WatchEmptyStateView: View {
|
|||||||
|
|
||||||
private struct WatchQRCodeCardView: View {
|
private struct WatchQRCodeCardView: View {
|
||||||
let card: WatchCard
|
let card: WatchCard
|
||||||
let qrService: WatchQRCodeService
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: WatchDesign.Spacing.small) {
|
VStack(spacing: WatchDesign.Spacing.small) {
|
||||||
@ -56,8 +54,8 @@ private struct WatchQRCodeCardView: View {
|
|||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundStyle(Color.WatchPalette.text)
|
.foregroundStyle(Color.WatchPalette.text)
|
||||||
|
|
||||||
if let image = qrService.qrCode(from: card.vCardPayload) {
|
if let image = card.qrCodeImage {
|
||||||
Image(decorative: image, scale: 1)
|
image
|
||||||
.resizable()
|
.resizable()
|
||||||
.interpolation(.none)
|
.interpolation(.none)
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
@ -65,6 +63,19 @@ private struct WatchQRCodeCardView: View {
|
|||||||
.padding(WatchDesign.Spacing.small)
|
.padding(WatchDesign.Spacing.small)
|
||||||
.background(Color.WatchPalette.card)
|
.background(Color.WatchPalette.card)
|
||||||
.clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.large))
|
.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)
|
||||||
|
.background(Color.WatchPalette.card)
|
||||||
|
.clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.large))
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(card.displayName)
|
Text(card.displayName)
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import CoreImage
|
|
||||||
import CoreImage.CIFilterBuiltins
|
|
||||||
import CoreGraphics
|
|
||||||
|
|
||||||
struct WatchQRCodeService {
|
|
||||||
private let context = CIContext()
|
|
||||||
|
|
||||||
func qrCode(from payload: String) -> CGImage? {
|
|
||||||
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 }
|
|
||||||
let scaledImage = outputImage.transformed(by: CGAffineTransform(scaleX: 10, y: 10))
|
|
||||||
return context.createCGImage(scaledImage, from: scaledImage.extent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
51
ROADMAP.md
51
ROADMAP.md
@ -67,22 +67,40 @@ This document tracks planned features and their implementation status.
|
|||||||
|
|
||||||
## 🔲 Planned Features
|
## 🔲 Planned Features
|
||||||
|
|
||||||
### Medium Priority (Differentiation)
|
### High Priority (Photo Sharing via App Clip)
|
||||||
|
|
||||||
- [ ] **Email signature export** - Generate HTML signature
|
- [ ] **App Clip for instant card sharing** - Recipients get full card with photo, no app install
|
||||||
- Generate professional HTML email signature from card data
|
- Solves the QR code size limitation for photos
|
||||||
- Copy to clipboard or share
|
- Recipient scans QR → App Clip loads instantly → "Add to Contacts" with photo
|
||||||
- Multiple signature styles/templates
|
- Uses CloudKit for ephemeral card storage (auto-deletes after save)
|
||||||
|
|
||||||
- [ ] **Card analytics** - View/scan counts
|
**Implementation phases:**
|
||||||
- Would require backend infrastructure
|
|
||||||
- Track when cards are viewed/scanned
|
|
||||||
- Show analytics dashboard
|
|
||||||
|
|
||||||
- [ ] **Virtual meeting background** - Generate image with QR
|
1. **CloudKit Setup**
|
||||||
- Create background image with card info + QR code
|
- [ ] Enable CloudKit capability
|
||||||
- Export for Zoom, Teams, etc.
|
- [ ] Create `SharedCard` record type (name, role, company, vCard data, photo asset)
|
||||||
- Multiple background styles
|
- [ ] Add `expiresAt` field for auto-cleanup
|
||||||
|
- [ ] Implement upload function in main app
|
||||||
|
- [ ] Implement cleanup (delete expired cards on app launch)
|
||||||
|
|
||||||
|
2. **App Clip Target**
|
||||||
|
- [ ] Create App Clip target (<15MB)
|
||||||
|
- [ ] Configure Associated Domains (`appclips:yourapp.com`)
|
||||||
|
- [ ] Build minimal UI: card preview + "Add to Contacts" button
|
||||||
|
- [ ] Fetch card from CloudKit using URL parameter
|
||||||
|
- [ ] Use `CNContactStore` to save contact with photo
|
||||||
|
- [ ] Call delete/mark-complete on CloudKit after save
|
||||||
|
|
||||||
|
3. **Main App Integration**
|
||||||
|
- [ ] New "Share via App Clip" option in ShareCardView
|
||||||
|
- [ ] Upload card to CloudKit on share
|
||||||
|
- [ ] Generate QR code with App Clip URL
|
||||||
|
- [ ] Show QR code for scanning
|
||||||
|
|
||||||
|
4. **App Store Configuration**
|
||||||
|
- [ ] Configure App Clip experience in App Store Connect
|
||||||
|
- [ ] Set up App Clip Code or Smart App Banner (optional)
|
||||||
|
- [ ] Test invocation URLs
|
||||||
|
|
||||||
### Lower Priority (Advanced)
|
### Lower Priority (Advanced)
|
||||||
|
|
||||||
@ -101,6 +119,11 @@ This document tracks planned features and their implementation status.
|
|||||||
- Requires backend infrastructure
|
- Requires backend infrastructure
|
||||||
- Team branding, shared templates
|
- Team branding, shared templates
|
||||||
|
|
||||||
|
- [ ] **Track share recipients** - Record who you shared your card with
|
||||||
|
- Add contact entry when sharing
|
||||||
|
- Track share date, method, and recipient info
|
||||||
|
- View share history per card
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔧 Technical Improvements
|
## 🔧 Technical Improvements
|
||||||
@ -134,4 +157,4 @@ This document tracks planned features and their implementation status.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Last updated: January 8, 2026*
|
*Last updated: January 9, 2026*
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user