Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
2b679b0167
commit
fa3cb3e017
11
BusinessCard.code-workspace
Normal file
11
BusinessCard.code-workspace
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../_Packages/Bedrock"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
@ -8,7 +8,9 @@
|
|||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
EA69DC822F3C199C00592220 /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA69DC812F3C199C00592220 /* Bedrock */; };
|
EA69DC822F3C199C00592220 /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA69DC812F3C199C00592220 /* Bedrock */; };
|
||||||
EA69E9272F3D4B5700592220 /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA69E9262F3D4B5700592220 /* Bedrock */; };
|
EA7568D32F4639EE006196BB /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA7568D22F4639EE006196BB /* Bedrock */; };
|
||||||
|
EA7568D52F463A28006196BB /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA7568D42F463A28006196BB /* Bedrock */; };
|
||||||
|
EA7568D72F463A2E006196BB /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA7568D62F463A2E006196BB /* Bedrock */; };
|
||||||
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 = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
EAAE892A2F12DE110075BC8A /* BusinessCardWatch Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = EA837F982F11B16400077F87 /* BusinessCardWatch Watch App.app */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||||
EACLIP0012F200000000001 /* BusinessCardClip.app in Embed App Clips */ = {isa = PBXBuildFile; fileRef = EACLIP0012F200000000002 /* BusinessCardClip.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
EACLIP0012F200000000001 /* BusinessCardClip.app in Embed App Clips */ = {isa = PBXBuildFile; fileRef = EACLIP0012F200000000002 /* BusinessCardClip.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
@ -150,6 +152,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
EA7568D32F4639EE006196BB /* Bedrock in Frameworks */,
|
||||||
EA837E672F107D6800077F87 /* Bedrock in Frameworks */,
|
EA837E672F107D6800077F87 /* Bedrock in Frameworks */,
|
||||||
EA69DC822F3C199C00592220 /* Bedrock in Frameworks */,
|
EA69DC822F3C199C00592220 /* Bedrock in Frameworks */,
|
||||||
);
|
);
|
||||||
@ -173,6 +176,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
EA7568D72F463A2E006196BB /* Bedrock in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -180,7 +184,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
EA69E9272F3D4B5700592220 /* Bedrock in Frameworks */,
|
EA7568D52F463A28006196BB /* Bedrock in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -255,6 +259,7 @@
|
|||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
EA837E662F107D6800077F87 /* Bedrock */,
|
EA837E662F107D6800077F87 /* Bedrock */,
|
||||||
EA69DC812F3C199C00592220 /* Bedrock */,
|
EA69DC812F3C199C00592220 /* Bedrock */,
|
||||||
|
EA7568D22F4639EE006196BB /* Bedrock */,
|
||||||
);
|
);
|
||||||
productName = BusinessCard;
|
productName = BusinessCard;
|
||||||
productReference = EA8379232F105F2600077F87 /* Business Card.app */;
|
productReference = EA8379232F105F2600077F87 /* Business Card.app */;
|
||||||
@ -323,6 +328,7 @@
|
|||||||
);
|
);
|
||||||
name = "BusinessCardWatch Watch App";
|
name = "BusinessCardWatch Watch App";
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
|
EA7568D62F463A2E006196BB /* Bedrock */,
|
||||||
);
|
);
|
||||||
productName = "BusinessCardWatch Watch App";
|
productName = "BusinessCardWatch Watch App";
|
||||||
productReference = EA837F982F11B16400077F87 /* BusinessCardWatch Watch App.app */;
|
productReference = EA837F982F11B16400077F87 /* BusinessCardWatch Watch App.app */;
|
||||||
@ -345,7 +351,7 @@
|
|||||||
);
|
);
|
||||||
name = BusinessCardClip;
|
name = BusinessCardClip;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
EA69E9262F3D4B5700592220 /* Bedrock */,
|
EA7568D42F463A28006196BB /* Bedrock */,
|
||||||
);
|
);
|
||||||
productName = BusinessCardClip;
|
productName = BusinessCardClip;
|
||||||
productReference = EACLIP0012F200000000002 /* BusinessCardClip.app */;
|
productReference = EACLIP0012F200000000002 /* BusinessCardClip.app */;
|
||||||
@ -392,7 +398,7 @@
|
|||||||
mainGroup = EA83791A2F105F2600077F87;
|
mainGroup = EA83791A2F105F2600077F87;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
EA69DC802F3C199C00592220 /* XCLocalSwiftPackageReference "../Bedrock" */,
|
EA7568D12F4639EE006196BB /* XCLocalSwiftPackageReference "../_Packages/Bedrock" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = EA8379242F105F2600077F87 /* Products */;
|
productRefGroup = EA8379242F105F2600077F87 /* Products */;
|
||||||
@ -1001,9 +1007,9 @@
|
|||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCLocalSwiftPackageReference section */
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
EA69DC802F3C199C00592220 /* XCLocalSwiftPackageReference "../Bedrock" */ = {
|
EA7568D12F4639EE006196BB /* XCLocalSwiftPackageReference "../_Packages/Bedrock" */ = {
|
||||||
isa = XCLocalSwiftPackageReference;
|
isa = XCLocalSwiftPackageReference;
|
||||||
relativePath = ../Bedrock;
|
relativePath = ../_Packages/Bedrock;
|
||||||
};
|
};
|
||||||
/* End XCLocalSwiftPackageReference section */
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
|
||||||
@ -1012,9 +1018,18 @@
|
|||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Bedrock;
|
productName = Bedrock;
|
||||||
};
|
};
|
||||||
EA69E9262F3D4B5700592220 /* Bedrock */ = {
|
EA7568D22F4639EE006196BB /* Bedrock */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = EA69DC802F3C199C00592220 /* XCLocalSwiftPackageReference "../Bedrock" */;
|
productName = Bedrock;
|
||||||
|
};
|
||||||
|
EA7568D42F463A28006196BB /* Bedrock */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = EA7568D12F4639EE006196BB /* XCLocalSwiftPackageReference "../_Packages/Bedrock" */;
|
||||||
|
productName = Bedrock;
|
||||||
|
};
|
||||||
|
EA7568D62F463A2E006196BB /* Bedrock */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = EA7568D12F4639EE006196BB /* XCLocalSwiftPackageReference "../_Packages/Bedrock" */;
|
||||||
productName = Bedrock;
|
productName = Bedrock;
|
||||||
};
|
};
|
||||||
EA837E662F107D6800077F87 /* Bedrock */ = {
|
EA837E662F107D6800077F87 /* Bedrock */ = {
|
||||||
|
|||||||
@ -53,7 +53,7 @@
|
|||||||
<CommandLineArguments>
|
<CommandLineArguments>
|
||||||
<CommandLineArgument
|
<CommandLineArgument
|
||||||
argument = "--clip-debug=preview"
|
argument = "--clip-debug=preview"
|
||||||
isEnabled = "YES">
|
isEnabled = "NO">
|
||||||
</CommandLineArgument>
|
</CommandLineArgument>
|
||||||
<CommandLineArgument
|
<CommandLineArgument
|
||||||
argument = "--clip-debug=loading"
|
argument = "--clip-debug=loading"
|
||||||
@ -68,6 +68,13 @@
|
|||||||
isEnabled = "NO">
|
isEnabled = "NO">
|
||||||
</CommandLineArgument>
|
</CommandLineArgument>
|
||||||
</CommandLineArguments>
|
</CommandLineArguments>
|
||||||
|
<EnvironmentVariables>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCAppClipURL"
|
||||||
|
value = "https://topdoglabs.com/bc?id=FE866C3C-EEBC-4A66-B176-11D0F01E28DF"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
</EnvironmentVariables>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
<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>
|
</dict>
|
||||||
<key>BusinessCardClip.xcscheme_^#shared#^_</key>
|
<key>BusinessCardClip.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
@ -17,7 +17,7 @@
|
|||||||
<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>2</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SuppressBuildableAutocreation</key>
|
<key>SuppressBuildableAutocreation</key>
|
||||||
|
|||||||
@ -817,6 +817,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Show first-run onboarding again on next app launch" : {
|
||||||
|
"comment" : "A description of the reset onboarding feature.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Social Media" : {
|
"Social Media" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,38 +1,38 @@
|
|||||||
{
|
{
|
||||||
"colors" : [
|
"colors": [
|
||||||
{
|
{
|
||||||
"color" : {
|
"color": {
|
||||||
"color-space" : "srgb",
|
"color-space": "srgb",
|
||||||
"components" : {
|
"components": {
|
||||||
"alpha" : "1.000",
|
"alpha": "1.000",
|
||||||
"blue" : "0.860",
|
"blue": "0.280",
|
||||||
"green" : "0.470",
|
"green": "0.330",
|
||||||
"red" : "0.220"
|
"red": "0.950"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom": "universal"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances" : [
|
"appearances": [
|
||||||
{
|
{
|
||||||
"appearance" : "luminosity",
|
"appearance": "luminosity",
|
||||||
"value" : "dark"
|
"value": "dark"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"color" : {
|
"color": {
|
||||||
"color-space" : "srgb",
|
"color-space": "srgb",
|
||||||
"components" : {
|
"components": {
|
||||||
"alpha" : "1.000",
|
"alpha": "1.000",
|
||||||
"blue" : "0.950",
|
"blue": "0.280",
|
||||||
"green" : "0.650",
|
"green": "0.330",
|
||||||
"red" : "0.350"
|
"red": "0.950"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom": "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info": {
|
||||||
"author" : "xcode",
|
"author": "xcode",
|
||||||
"version" : 1
|
"version": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import AppIntents
|
import AppIntents
|
||||||
|
import Bedrock
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct BusinessCardClipApp: App {
|
struct BusinessCardClipApp: App {
|
||||||
@ -37,30 +38,50 @@ struct BusinessCardClipApp: App {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
|
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
|
||||||
|
Design.debugLog("Clip: onContinueUserActivity fired, webpageURL=\(activity.webpageURL?.absoluteString ?? "nil")")
|
||||||
handleUserActivity(activity)
|
handleUserActivity(activity)
|
||||||
}
|
}
|
||||||
.onOpenURL { url in
|
.onOpenURL { url in
|
||||||
|
Design.debugLog("Clip: onOpenURL fired, url=\(url.absoluteString)")
|
||||||
handleURL(url)
|
handleURL(url)
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
|
Design.debugLog("Clip: .task running - checking launch sources")
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
debugState = parseDebugStateArgument()
|
if let dbg = parseDebugStateArgument() {
|
||||||
|
Design.debugLog("Clip: debug state from args: \(dbg.rawValue)")
|
||||||
|
debugState = dbg
|
||||||
|
return
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
if let envURL = ProcessInfo.processInfo.environment["_XCAppClipURL"],
|
||||||
|
let url = URL(string: envURL) {
|
||||||
|
Design.debugLog("Clip: _XCAppClipURL from env: \(envURL)")
|
||||||
|
handleURL(url)
|
||||||
|
} else {
|
||||||
|
Design.debugLog("Clip: no _XCAppClipURL in env, no debug args. recordName=\(recordName ?? "nil")")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleUserActivity(_ activity: NSUserActivity) {
|
private func handleUserActivity(_ activity: NSUserActivity) {
|
||||||
guard let url = activity.webpageURL else { return }
|
Design.debugLog("Clip: handleUserActivity, webpageURL=\(activity.webpageURL?.absoluteString ?? "nil")")
|
||||||
|
guard let url = activity.webpageURL else {
|
||||||
|
Design.debugLog("Clip: handleUserActivity - no webpageURL, skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
handleURL(url)
|
handleURL(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleURL(_ url: URL) {
|
private func handleURL(_ url: URL) {
|
||||||
|
Design.debugLog("Clip: handleURL \(url.absoluteString)")
|
||||||
guard let id = extractRecordName(from: url) else {
|
guard let id = extractRecordName(from: url) else {
|
||||||
|
Design.debugLog("Clip: extractRecordName failed for \(url.absoluteString)")
|
||||||
launchErrorMessage = ClipError.invalidRecord.localizedDescription
|
launchErrorMessage = ClipError.invalidRecord.localizedDescription
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
Design.debugLog("Clip: extracted recordName=\(id), setting recordName")
|
||||||
launchErrorMessage = nil
|
launchErrorMessage = nil
|
||||||
recordName = id
|
recordName = id
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,12 +43,12 @@ final class ClipCardStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Saves the currently loaded card to Contacts.
|
/// Saves the currently loaded card to Contacts via CNContactStore.
|
||||||
|
/// Note: App Clips cannot use CNContactStore; use the share sheet flow instead.
|
||||||
func saveToContacts() async {
|
func saveToContacts() async {
|
||||||
guard let snapshot else { return }
|
guard let snapshot else { return }
|
||||||
do {
|
do {
|
||||||
try await contactSave.saveContact(vCardData: snapshot.vCardData)
|
try await contactSave.saveContact(vCardData: snapshot.vCardData)
|
||||||
// Best-effort cleanup so saved cards do not linger in public CloudKit.
|
|
||||||
try? await cloudKit.deleteSharedCard(recordName: snapshot.recordName)
|
try? await cloudKit.deleteSharedCard(recordName: snapshot.recordName)
|
||||||
state = .saved
|
state = .saved
|
||||||
} catch {
|
} catch {
|
||||||
@ -56,6 +56,13 @@ final class ClipCardStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Best-effort cleanup after user shares the vCard (e.g. via share sheet).
|
||||||
|
/// App Clips use the share sheet instead of CNContactStore.
|
||||||
|
func cleanupAfterShare() async {
|
||||||
|
guard let snapshot else { return }
|
||||||
|
try? await cloudKit.deleteSharedCard(recordName: snapshot.recordName)
|
||||||
|
}
|
||||||
|
|
||||||
private func userMessage(for error: Error, fallback: ClipError) -> String {
|
private func userMessage(for error: Error, fallback: ClipError) -> String {
|
||||||
if let clipError = error as? ClipError {
|
if let clipError = error as? ClipError {
|
||||||
return clipError.localizedDescription
|
return clipError.localizedDescription
|
||||||
|
|||||||
@ -16,7 +16,7 @@ struct ClipRootView: View {
|
|||||||
ClipLoadingView()
|
ClipLoadingView()
|
||||||
case .loaded(let snapshot):
|
case .loaded(let snapshot):
|
||||||
ClipCardPreview(snapshot: snapshot) {
|
ClipCardPreview(snapshot: snapshot) {
|
||||||
Task { await store.saveToContacts() }
|
Task { await store.cleanupAfterShare() }
|
||||||
}
|
}
|
||||||
case .saved:
|
case .saved:
|
||||||
ClipSuccessView()
|
ClipSuccessView()
|
||||||
|
|||||||
@ -9,24 +9,25 @@ struct ClipCardPreview: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let snapshot: SharedCardSnapshot
|
let snapshot: SharedCardSnapshot
|
||||||
|
/// Called when share sheet is dismissed; use for CloudKit cleanup.
|
||||||
let onSave: () -> Void
|
let onSave: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
@State private var showShareSheet = false
|
||||||
VStack(spacing: ClipDesign.Spacing.xLarge) {
|
|
||||||
Spacer(minLength: ClipDesign.Spacing.medium)
|
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView(showsIndicators: false) {
|
||||||
|
VStack(spacing: ClipDesign.Spacing.xLarge) {
|
||||||
previewCard
|
previewCard
|
||||||
.frame(maxWidth: ClipDesign.Size.previewMaxWidth)
|
.frame(maxWidth: ClipDesign.Size.previewMaxWidth)
|
||||||
.padding(.horizontal, ClipDesign.Spacing.large)
|
|
||||||
|
|
||||||
VStack(spacing: ClipDesign.Spacing.medium) {
|
VStack(spacing: ClipDesign.Spacing.medium) {
|
||||||
// Save button
|
// Add to Contacts - uses share sheet because App Clips cannot use CNContactStore
|
||||||
ClipPrimaryButton(
|
ClipPrimaryButton(
|
||||||
title: String(localized: "Save to Contacts"),
|
title: String(localized: "Add to Contacts"),
|
||||||
systemImage: "person.crop.circle.badge.plus",
|
systemImage: "person.crop.circle.badge.plus",
|
||||||
action: onSave
|
action: { showShareSheet = true }
|
||||||
)
|
)
|
||||||
.accessibilityLabel(Text("Save \(snapshot.displayName) to contacts"))
|
.accessibilityLabel(Text("Add \(snapshot.displayName) to contacts"))
|
||||||
|
|
||||||
// Get full app prompt
|
// Get full app prompt
|
||||||
Button {
|
Button {
|
||||||
@ -38,8 +39,14 @@ struct ClipCardPreview: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, ClipDesign.Spacing.xLarge)
|
.padding(.horizontal, ClipDesign.Spacing.xLarge)
|
||||||
|
}
|
||||||
Spacer(minLength: ClipDesign.Spacing.large)
|
.padding(.horizontal, ClipDesign.Spacing.large)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showShareSheet) {
|
||||||
|
ClipShareSheet(vCardData: snapshot.vCardData) {
|
||||||
|
showShareSheet = false
|
||||||
|
onSave()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@ struct ClipPrimaryButton: View {
|
|||||||
HStack(spacing: ClipDesign.Spacing.small) {
|
HStack(spacing: ClipDesign.Spacing.small) {
|
||||||
if let systemImage {
|
if let systemImage {
|
||||||
Image(systemName: systemImage)
|
Image(systemName: systemImage)
|
||||||
|
.foregroundStyle(Color.Clip.background)
|
||||||
}
|
}
|
||||||
Text(title)
|
Text(title)
|
||||||
.styled(.headingEmphasis, emphasis: .custom(Color.Clip.background))
|
.styled(.headingEmphasis, emphasis: .custom(Color.Clip.background))
|
||||||
|
|||||||
33
BusinessCardClip/Views/Components/ClipShareSheet.swift
Normal file
33
BusinessCardClip/Views/Components/ClipShareSheet.swift
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// Presents a share sheet with a vCard file so the user can add to Contacts.
|
||||||
|
/// App Clips cannot use CNContactStore; the share sheet's "Add to Contacts" option works instead.
|
||||||
|
struct ClipShareSheet: UIViewControllerRepresentable {
|
||||||
|
let vCardData: String
|
||||||
|
let onDismiss: (() -> Void)?
|
||||||
|
|
||||||
|
init(vCardData: String, onDismiss: (() -> Void)? = nil) {
|
||||||
|
self.vCardData = vCardData
|
||||||
|
self.onDismiss = onDismiss
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> UIActivityViewController {
|
||||||
|
let tempURL = FileManager.default.temporaryDirectory
|
||||||
|
.appendingPathComponent("contact-\(UUID().uuidString).vcf")
|
||||||
|
let data = Data(vCardData.utf8)
|
||||||
|
try? data.write(to: tempURL)
|
||||||
|
|
||||||
|
let controller = UIActivityViewController(
|
||||||
|
activityItems: [tempURL],
|
||||||
|
applicationActivities: nil
|
||||||
|
)
|
||||||
|
controller.completionWithItemsHandler = { _, _, _, _ in
|
||||||
|
try? FileManager.default.removeItem(at: tempURL)
|
||||||
|
onDismiss?()
|
||||||
|
}
|
||||||
|
return controller
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user