feat(live): Live Activity / Dynamic Island (WidgetKit extension iOS 16.2+/17+)
Nouvelle target "CoachLiveActivity" (Widget Extension, bundle ch.hypnotruck.coach.LiveActivity, iOS 17.0) embarquée dans App via copy phase "Embed App Extensions" (dstSubfolderSpec=13). API ActivityKit + WidgetKit vérifiées sur SDK iOS 26.5 swiftinterface (Activity.request(content:), ActivityConfiguration, DynamicIsland(expanded:compactLeading:trailing:minimal:)). Architecture : - CoachLiveActivityAttributes (modèle partagé App ↔ extension) - CoachLiveActivityWidget (Lock Screen + Dynamic Island UI) - CoachLiveActivityBundle (@main WidgetBundle) - LiveActivityManager (App side, wrap Activity.request/update/end) - LiveStore hook : start sur 1er sample, update à chaque ingest, end après 60s "lost". Tout guardé @available(iOS 16.2, *) pour préserver deployment iOS 15. - Info.plist App : NSSupportsLiveActivities = YES. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
01468471ECA28EA66AFE2D6C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0EB6C76F68679844EC72178F /* Foundation.framework */; };
|
01468471ECA28EA66AFE2D6C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0EB6C76F68679844EC72178F /* Foundation.framework */; };
|
||||||
0318E7E610501FC1F958251B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3342ACCE085010828CF63565 /* Assets.xcassets */; };
|
0318E7E610501FC1F958251B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3342ACCE085010828CF63565 /* Assets.xcassets */; };
|
||||||
|
1B690F0D16AB76A6854CA1F7 /* CoachLiveActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431C7B10248D8EED4D397AB0 /* CoachLiveActivityAttributes.swift */; };
|
||||||
21B242F8E61FB07C476C477C /* CoachLiveBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DF3346C49A690C3C3DF2F3 /* CoachLiveBridge.swift */; };
|
21B242F8E61FB07C476C477C /* CoachLiveBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DF3346C49A690C3C3DF2F3 /* CoachLiveBridge.swift */; };
|
||||||
290D0F17978B7EF3DDB3A792 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A06CDFF5B4F7852B6329862 /* ContentView.swift */; };
|
290D0F17978B7EF3DDB3A792 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A06CDFF5B4F7852B6329862 /* ContentView.swift */; };
|
||||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
||||||
@@ -23,12 +24,18 @@
|
|||||||
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||||
543A7005429A43F54C0EC61B /* CoachWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6BEAEF6F6274D61CAF22636 /* CoachWatchApp.swift */; };
|
543A7005429A43F54C0EC61B /* CoachWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6BEAEF6F6274D61CAF22636 /* CoachWatchApp.swift */; };
|
||||||
548F8BFA06417B35474BD69E /* CoachLiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340B99D5EA4DB985D7DFDA10 /* CoachLiveView.swift */; };
|
548F8BFA06417B35474BD69E /* CoachLiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340B99D5EA4DB985D7DFDA10 /* CoachLiveView.swift */; };
|
||||||
|
694B2F69591CAA85CE5DEC3A /* CoachLiveActivityWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBBF61A35F291A021BA75C60 /* CoachLiveActivityWidget.swift */; };
|
||||||
88F28EB12FBB330E00E8306E /* CoachWorkoutObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88F28EB02FBB330E00E8306E /* CoachWorkoutObserver.swift */; };
|
88F28EB12FBB330E00E8306E /* CoachWorkoutObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88F28EB02FBB330E00E8306E /* CoachWorkoutObserver.swift */; };
|
||||||
97116C31D4CC40F71A6A0FE7 /* LiveStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6030BD533EBC2D1D26DB9479 /* LiveStore.swift */; };
|
97116C31D4CC40F71A6A0FE7 /* LiveStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6030BD533EBC2D1D26DB9479 /* LiveStore.swift */; };
|
||||||
|
9BED4C04C38FCB8634CE2A94 /* CoachLiveActivity.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 9B5664FCDB4DDE62514D3A1E /* CoachLiveActivity.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
|
9E88D51C0B0255D24274DACC /* CoachLiveActivityBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BA75EEC3CBF1F46A9028C66 /* CoachLiveActivityBundle.swift */; };
|
||||||
B06AB24B350B221F38D99E17 /* ConnectivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A420239FC6D065C8A599B61C /* ConnectivityManager.swift */; };
|
B06AB24B350B221F38D99E17 /* ConnectivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A420239FC6D065C8A599B61C /* ConnectivityManager.swift */; };
|
||||||
C0AC4A07811751FB78AA0001 /* CoachAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0AC4A07811751FB78AA0002 /* CoachAuth.swift */; };
|
C0AC4A07811751FB78AA0001 /* CoachAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0AC4A07811751FB78AA0002 /* CoachAuth.swift */; };
|
||||||
|
D1384CFA4960060CDBF05FC3 /* CoachLiveActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431C7B10248D8EED4D397AB0 /* CoachLiveActivityAttributes.swift */; };
|
||||||
D1BD5B1990CF3F2B9C5D0001 /* CoachWorkoutKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BD5B1990CF3F2B9C5D0002 /* CoachWorkoutKit.swift */; };
|
D1BD5B1990CF3F2B9C5D0001 /* CoachWorkoutKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BD5B1990CF3F2B9C5D0002 /* CoachWorkoutKit.swift */; };
|
||||||
|
E1E8BD7CF9C19E56CE0F57C4 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61D7B2F5AFEF834423E07AB /* Foundation.framework */; };
|
||||||
E2CE6C2AA1DF4F3CAD6E0001 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CE6C2AA1DF4F3CAD6E0002 /* MainViewController.swift */; };
|
E2CE6C2AA1DF4F3CAD6E0001 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CE6C2AA1DF4F3CAD6E0002 /* MainViewController.swift */; };
|
||||||
|
E547F675B2F61C3475D2A787 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93DA6B121E94B11DF938C90 /* LiveActivityManager.swift */; };
|
||||||
F1ABCDEF0123456789AB0001 /* CoachHealthRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1ABCDEF0123456789AB0002 /* CoachHealthRoute.swift */; };
|
F1ABCDEF0123456789AB0001 /* CoachHealthRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1ABCDEF0123456789AB0002 /* CoachHealthRoute.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
@@ -40,9 +47,27 @@
|
|||||||
remoteGlobalIDString = D5BF41648D334DC24770B9D2;
|
remoteGlobalIDString = D5BF41648D334DC24770B9D2;
|
||||||
remoteInfo = CoachWatch;
|
remoteInfo = CoachWatch;
|
||||||
};
|
};
|
||||||
|
9D160400D2AE8723B1112F2F /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 504EC2FC1FED79650016851F /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 28FB3582F417B2F3C6BFC5D1;
|
||||||
|
remoteInfo = CoachLiveActivity;
|
||||||
|
};
|
||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
C4916F81E61FABAAD62EE4F9 /* Embed App Extensions */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 13;
|
||||||
|
files = (
|
||||||
|
9BED4C04C38FCB8634CE2A94 /* CoachLiveActivity.appex in Embed App Extensions */,
|
||||||
|
);
|
||||||
|
name = "Embed App Extensions";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
F50D9A7DFABB52EE96BB93D6 /* Embed Watch Content */ = {
|
F50D9A7DFABB52EE96BB93D6 /* Embed Watch Content */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -58,10 +83,12 @@
|
|||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
0EB6C76F68679844EC72178F /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS11.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
|
0EB6C76F68679844EC72178F /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS11.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
|
||||||
|
1BA75EEC3CBF1F46A9028C66 /* CoachLiveActivityBundle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoachLiveActivityBundle.swift; sourceTree = "<group>"; };
|
||||||
2517374227A4072A4D6AFC15 /* CoachWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CoachWatch.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
2517374227A4072A4D6AFC15 /* CoachWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CoachWatch.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
||||||
3342ACCE085010828CF63565 /* Assets.xcassets */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
3342ACCE085010828CF63565 /* Assets.xcassets */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
340B99D5EA4DB985D7DFDA10 /* CoachLiveView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoachLiveView.swift; sourceTree = "<group>"; };
|
340B99D5EA4DB985D7DFDA10 /* CoachLiveView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoachLiveView.swift; sourceTree = "<group>"; };
|
||||||
|
431C7B10248D8EED4D397AB0 /* CoachLiveActivityAttributes.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoachLiveActivityAttributes.swift; sourceTree = "<group>"; };
|
||||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||||
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
@@ -77,14 +104,19 @@
|
|||||||
8A06CDFF5B4F7852B6329862 /* ContentView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
8A06CDFF5B4F7852B6329862 /* ContentView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
8AC9C9B9971F55027D227A06 /* WorkoutManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WorkoutManager.swift; sourceTree = "<group>"; };
|
8AC9C9B9971F55027D227A06 /* WorkoutManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WorkoutManager.swift; sourceTree = "<group>"; };
|
||||||
958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; };
|
958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; };
|
||||||
|
9B5664FCDB4DDE62514D3A1E /* CoachLiveActivity.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = CoachLiveActivity.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
A420239FC6D065C8A599B61C /* ConnectivityManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConnectivityManager.swift; sourceTree = "<group>"; };
|
A420239FC6D065C8A599B61C /* ConnectivityManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConnectivityManager.swift; sourceTree = "<group>"; };
|
||||||
A9DF3346C49A690C3C3DF2F3 /* CoachLiveBridge.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoachLiveBridge.swift; sourceTree = "<group>"; };
|
A9DF3346C49A690C3C3DF2F3 /* CoachLiveBridge.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoachLiveBridge.swift; sourceTree = "<group>"; };
|
||||||
|
B93DA6B121E94B11DF938C90 /* LiveActivityManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = "<group>"; };
|
||||||
C0AC4A07811751FB78AA0002 /* CoachAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachAuth.swift; sourceTree = "<group>"; };
|
C0AC4A07811751FB78AA0002 /* CoachAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachAuth.swift; sourceTree = "<group>"; };
|
||||||
D09B589FA572B2C14E8CD835 /* CoachWatch.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = CoachWatch.entitlements; sourceTree = "<group>"; };
|
D09B589FA572B2C14E8CD835 /* CoachWatch.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = CoachWatch.entitlements; sourceTree = "<group>"; };
|
||||||
D1BD5B1990CF3F2B9C5D0002 /* CoachWorkoutKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachWorkoutKit.swift; sourceTree = "<group>"; };
|
D1BD5B1990CF3F2B9C5D0002 /* CoachWorkoutKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachWorkoutKit.swift; sourceTree = "<group>"; };
|
||||||
|
D61D7B2F5AFEF834423E07AB /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
|
||||||
E2CE6C2AA1DF4F3CAD6E0002 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
|
E2CE6C2AA1DF4F3CAD6E0002 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
|
||||||
|
EBBF61A35F291A021BA75C60 /* CoachLiveActivityWidget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoachLiveActivityWidget.swift; sourceTree = "<group>"; };
|
||||||
F1ABCDEF0123456789AB0002 /* CoachHealthRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachHealthRoute.swift; sourceTree = "<group>"; };
|
F1ABCDEF0123456789AB0002 /* CoachHealthRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachHealthRoute.swift; sourceTree = "<group>"; };
|
||||||
F6BEAEF6F6274D61CAF22636 /* CoachWatchApp.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoachWatchApp.swift; sourceTree = "<group>"; };
|
F6BEAEF6F6274D61CAF22636 /* CoachWatchApp.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoachWatchApp.swift; sourceTree = "<group>"; };
|
||||||
|
FBFCA969A1B083CD3178D9B2 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -104,6 +136,14 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
D8B443A150F5A7E1C135ABAA /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
E1E8BD7CF9C19E56CE0F57C4 /* Foundation.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
@@ -115,6 +155,18 @@
|
|||||||
name = watchOS;
|
name = watchOS;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
460F58BF290358F572689016 /* CoachLiveActivity */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
431C7B10248D8EED4D397AB0 /* CoachLiveActivityAttributes.swift */,
|
||||||
|
EBBF61A35F291A021BA75C60 /* CoachLiveActivityWidget.swift */,
|
||||||
|
1BA75EEC3CBF1F46A9028C66 /* CoachLiveActivityBundle.swift */,
|
||||||
|
FBFCA969A1B083CD3178D9B2 /* Info.plist */,
|
||||||
|
);
|
||||||
|
name = CoachLiveActivity;
|
||||||
|
path = CoachLiveActivity;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
504EC2FB1FED79650016851F = {
|
504EC2FB1FED79650016851F = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -123,6 +175,7 @@
|
|||||||
504EC3051FED79650016851F /* Products */,
|
504EC3051FED79650016851F /* Products */,
|
||||||
B91ACF72653FD495148698CF /* Frameworks */,
|
B91ACF72653FD495148698CF /* Frameworks */,
|
||||||
5A8EA0CF92316E437E9053A0 /* CoachWatch */,
|
5A8EA0CF92316E437E9053A0 /* CoachWatch */,
|
||||||
|
460F58BF290358F572689016 /* CoachLiveActivity */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -131,6 +184,7 @@
|
|||||||
children = (
|
children = (
|
||||||
504EC3041FED79650016851F /* App.app */,
|
504EC3041FED79650016851F /* App.app */,
|
||||||
2517374227A4072A4D6AFC15 /* CoachWatch.app */,
|
2517374227A4072A4D6AFC15 /* CoachWatch.app */,
|
||||||
|
9B5664FCDB4DDE62514D3A1E /* CoachLiveActivity.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -155,6 +209,7 @@
|
|||||||
A9DF3346C49A690C3C3DF2F3 /* CoachLiveBridge.swift */,
|
A9DF3346C49A690C3C3DF2F3 /* CoachLiveBridge.swift */,
|
||||||
6030BD533EBC2D1D26DB9479 /* LiveStore.swift */,
|
6030BD533EBC2D1D26DB9479 /* LiveStore.swift */,
|
||||||
340B99D5EA4DB985D7DFDA10 /* CoachLiveView.swift */,
|
340B99D5EA4DB985D7DFDA10 /* CoachLiveView.swift */,
|
||||||
|
B93DA6B121E94B11DF938C90 /* LiveActivityManager.swift */,
|
||||||
);
|
);
|
||||||
path = App;
|
path = App;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -173,10 +228,19 @@
|
|||||||
path = CoachWatch;
|
path = CoachWatch;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
7497026ECEFF81CCD11C289D /* iOS */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D61D7B2F5AFEF834423E07AB /* Foundation.framework */,
|
||||||
|
);
|
||||||
|
name = iOS;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
B91ACF72653FD495148698CF /* Frameworks */ = {
|
B91ACF72653FD495148698CF /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
3E58B60E9B4F1CA14B5EC667 /* watchOS */,
|
3E58B60E9B4F1CA14B5EC667 /* watchOS */,
|
||||||
|
7497026ECEFF81CCD11C289D /* iOS */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -184,6 +248,23 @@
|
|||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
28FB3582F417B2F3C6BFC5D1 /* CoachLiveActivity */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = C2671D2741138A4A55CE1ED0 /* Build configuration list for PBXNativeTarget "CoachLiveActivity" */;
|
||||||
|
buildPhases = (
|
||||||
|
2D25180D64B3326AE6F9D91B /* Sources */,
|
||||||
|
D8B443A150F5A7E1C135ABAA /* Frameworks */,
|
||||||
|
411CB19BB5DB127758A86A2D /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = CoachLiveActivity;
|
||||||
|
productName = CoachLiveActivity;
|
||||||
|
productReference = 9B5664FCDB4DDE62514D3A1E /* CoachLiveActivity.appex */;
|
||||||
|
productType = "com.apple.product-type.app-extension";
|
||||||
|
};
|
||||||
504EC3031FED79650016851F /* App */ = {
|
504EC3031FED79650016851F /* App */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
|
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
|
||||||
@@ -192,11 +273,13 @@
|
|||||||
504EC3011FED79650016851F /* Frameworks */,
|
504EC3011FED79650016851F /* Frameworks */,
|
||||||
504EC3021FED79650016851F /* Resources */,
|
504EC3021FED79650016851F /* Resources */,
|
||||||
F50D9A7DFABB52EE96BB93D6 /* Embed Watch Content */,
|
F50D9A7DFABB52EE96BB93D6 /* Embed Watch Content */,
|
||||||
|
C4916F81E61FABAAD62EE4F9 /* Embed App Extensions */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
EEEAB45810829EE8B3C4976E /* PBXTargetDependency */,
|
EEEAB45810829EE8B3C4976E /* PBXTargetDependency */,
|
||||||
|
BACE810C3CD85E302BBFEC84 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = App;
|
name = App;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
@@ -258,11 +341,19 @@
|
|||||||
targets = (
|
targets = (
|
||||||
504EC3031FED79650016851F /* App */,
|
504EC3031FED79650016851F /* App */,
|
||||||
D5BF41648D334DC24770B9D2 /* CoachWatch */,
|
D5BF41648D334DC24770B9D2 /* CoachWatch */,
|
||||||
|
28FB3582F417B2F3C6BFC5D1 /* CoachLiveActivity */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
411CB19BB5DB127758A86A2D /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
504EC3021FED79650016851F /* Resources */ = {
|
504EC3021FED79650016851F /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -287,6 +378,16 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
2D25180D64B3326AE6F9D91B /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
1B690F0D16AB76A6854CA1F7 /* CoachLiveActivityAttributes.swift in Sources */,
|
||||||
|
694B2F69591CAA85CE5DEC3A /* CoachLiveActivityWidget.swift in Sources */,
|
||||||
|
9E88D51C0B0255D24274DACC /* CoachLiveActivityBundle.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
504EC3001FED79650016851F /* Sources */ = {
|
504EC3001FED79650016851F /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -300,6 +401,8 @@
|
|||||||
21B242F8E61FB07C476C477C /* CoachLiveBridge.swift in Sources */,
|
21B242F8E61FB07C476C477C /* CoachLiveBridge.swift in Sources */,
|
||||||
97116C31D4CC40F71A6A0FE7 /* LiveStore.swift in Sources */,
|
97116C31D4CC40F71A6A0FE7 /* LiveStore.swift in Sources */,
|
||||||
548F8BFA06417B35474BD69E /* CoachLiveView.swift in Sources */,
|
548F8BFA06417B35474BD69E /* CoachLiveView.swift in Sources */,
|
||||||
|
D1384CFA4960060CDBF05FC3 /* CoachLiveActivityAttributes.swift in Sources */,
|
||||||
|
E547F675B2F61C3475D2A787 /* LiveActivityManager.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -317,6 +420,12 @@
|
|||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
/* Begin PBXTargetDependency section */
|
||||||
|
BACE810C3CD85E302BBFEC84 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
name = CoachLiveActivity;
|
||||||
|
target = 28FB3582F417B2F3C6BFC5D1 /* CoachLiveActivity */;
|
||||||
|
targetProxy = 9D160400D2AE8723B1112F2F /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
EEEAB45810829EE8B3C4976E /* PBXTargetDependency */ = {
|
EEEAB45810829EE8B3C4976E /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
name = CoachWatch;
|
name = CoachWatch;
|
||||||
@@ -575,6 +684,63 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
90CE3C22F60C6432852DC1B1 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = "";
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 22;
|
||||||
|
DEVELOPMENT_TEAM = TZ2PVTRKNY;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
|
INFOPLIST_FILE = CoachLiveActivity/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.2;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = ch.hypnotruck.coach.LiveActivity;
|
||||||
|
PRODUCT_NAME = CoachLiveActivity;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
C7A0838F3DA0842EDE320050 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = "";
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 22;
|
||||||
|
DEVELOPMENT_TEAM = TZ2PVTRKNY;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
|
INFOPLIST_FILE = CoachLiveActivity/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.2;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = ch.hypnotruck.coach.LiveActivity;
|
||||||
|
PRODUCT_NAME = CoachLiveActivity;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
@@ -596,6 +762,15 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
C2671D2741138A4A55CE1ED0 /* Build configuration list for PBXNativeTarget "CoachLiveActivity" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
C7A0838F3DA0842EDE320050 /* Release */,
|
||||||
|
90CE3C22F60C6432852DC1B1 /* Debug */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
FA2A9602402AA384726BFF97 /* Build configuration list for PBXNativeTarget "CoachWatch" */ = {
|
FA2A9602402AA384726BFF97 /* Build configuration list for PBXNativeTarget "CoachWatch" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
|||||||
@@ -38,6 +38,8 @@
|
|||||||
<string>Coach Hypnotruck utilise votre position pour tracer vos sorties course et VTT.</string>
|
<string>Coach Hypnotruck utilise votre position pour tracer vos sorties course et VTT.</string>
|
||||||
<key>NSMotionUsageDescription</key>
|
<key>NSMotionUsageDescription</key>
|
||||||
<string>Coach Hypnotruck utilise les capteurs de mouvement pour mesurer votre activité.</string>
|
<string>Coach Hypnotruck utilise les capteurs de mouvement pour mesurer votre activité.</string>
|
||||||
|
<key>NSSupportsLiveActivities</key>
|
||||||
|
<true/>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>Coach Hypnotruck peut accéder à vos photos pour ajouter une photo de profil.</string>
|
<string>Coach Hypnotruck peut accéder à vos photos pour ajouter une photo de profil.</string>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
|
|||||||
77
ios/App/App/LiveActivityManager.swift
Normal file
77
ios/App/App/LiveActivityManager.swift
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// LiveActivityManager.swift
|
||||||
|
// Wrapper main-actor autour d'ActivityKit pour la Live Activity / Dynamic
|
||||||
|
// Island du live workout. Appelé par LiveStore à chaque transition / sample.
|
||||||
|
// API ActivityKit vérifiée sur iOS 26.5 SDK swiftinterface :
|
||||||
|
// Activity.request(attributes:, content: ActivityContent<...>, pushType:)
|
||||||
|
// activity.update(_ content: ActivityContent<...>) async
|
||||||
|
// activity.end(_ content: ActivityContent<...>?, dismissalPolicy:) async
|
||||||
|
// ActivityAuthorizationInfo().areActivitiesEnabled
|
||||||
|
|
||||||
|
import ActivityKit
|
||||||
|
import Foundation
|
||||||
|
import os
|
||||||
|
|
||||||
|
@available(iOS 16.2, *)
|
||||||
|
@MainActor
|
||||||
|
final class LiveActivityManager {
|
||||||
|
static let shared = LiveActivityManager()
|
||||||
|
private var current: Activity<CoachLiveActivityAttributes>?
|
||||||
|
private static let log = Logger(subsystem: "ch.hypnotruck.coach", category: "live-activity")
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
private var isAuthorized: Bool {
|
||||||
|
ActivityAuthorizationInfo().areActivitiesEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Démarre l'activité si pas déjà en cours. Si déjà en cours, met à jour.
|
||||||
|
func start(label: String,
|
||||||
|
symbol: String,
|
||||||
|
state: CoachLiveActivityAttributes.ContentState) {
|
||||||
|
if current != nil {
|
||||||
|
update(state: state)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard isAuthorized else {
|
||||||
|
Self.log.warning("Live Activities not authorized (user disabled in Settings)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let attrs = CoachLiveActivityAttributes(
|
||||||
|
activityLabel: label,
|
||||||
|
activitySymbol: symbol,
|
||||||
|
startedAt: Date()
|
||||||
|
)
|
||||||
|
let content = ActivityContent(
|
||||||
|
state: state,
|
||||||
|
staleDate: Date().addingTimeInterval(60)
|
||||||
|
)
|
||||||
|
do {
|
||||||
|
current = try Activity.request(
|
||||||
|
attributes: attrs,
|
||||||
|
content: content,
|
||||||
|
pushType: nil
|
||||||
|
)
|
||||||
|
Self.log.info("Live Activity started: \(label, privacy: .public)")
|
||||||
|
} catch {
|
||||||
|
Self.log.error("Failed to start Live Activity: \(error.localizedDescription, privacy: .public)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Met à jour l'activité en cours (no-op si pas démarrée).
|
||||||
|
func update(state: CoachLiveActivityAttributes.ContentState) {
|
||||||
|
guard let activity = current else { return }
|
||||||
|
let content = ActivityContent(
|
||||||
|
state: state,
|
||||||
|
staleDate: Date().addingTimeInterval(60)
|
||||||
|
)
|
||||||
|
Task { await activity.update(content) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Termine l'activité. dismissalPolicy .default = bannière reste ~4h.
|
||||||
|
func end() {
|
||||||
|
guard let activity = current else { return }
|
||||||
|
current = nil
|
||||||
|
Task { await activity.end(nil, dismissalPolicy: .default) }
|
||||||
|
Self.log.info("Live Activity ended")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,14 +27,31 @@ final class LiveStore: ObservableObject {
|
|||||||
|
|
||||||
private var watchdog: Timer?
|
private var watchdog: Timer?
|
||||||
private let lostThreshold: TimeInterval = 8
|
private let lostThreshold: TimeInterval = 8
|
||||||
|
private let endAfterLost: TimeInterval = 60
|
||||||
|
private var lostSince: Date?
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
watchdog = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
watchdog = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
||||||
Task { @MainActor [weak self] in
|
Task { @MainActor [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
guard self.status == .live, let ts = self.lastSampleAt else { return }
|
guard let ts = self.lastSampleAt else { return }
|
||||||
if Date().timeIntervalSince(ts) > self.lostThreshold {
|
let sinceLast = Date().timeIntervalSince(ts)
|
||||||
|
if self.status == .live, sinceLast > self.lostThreshold {
|
||||||
|
// Transition live -> lost : signale à la Live Activity
|
||||||
|
// que la connexion est perdue (heartbeat off, statut orange).
|
||||||
self.status = .lost
|
self.status = .lost
|
||||||
|
self.lostSince = Date()
|
||||||
|
if #available(iOS 16.2, *) {
|
||||||
|
LiveActivityManager.shared.update(state: self.currentState(isLive: false))
|
||||||
|
}
|
||||||
|
} else if self.status == .lost,
|
||||||
|
let lostAt = self.lostSince,
|
||||||
|
Date().timeIntervalSince(lostAt) > self.endAfterLost {
|
||||||
|
// Resté .lost trop longtemps : on termine l'activité.
|
||||||
|
if #available(iOS 16.2, *) {
|
||||||
|
LiveActivityManager.shared.end()
|
||||||
|
}
|
||||||
|
self.lostSince = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,6 +68,7 @@ final class LiveStore: ObservableObject {
|
|||||||
let act = sample["activityType"] as? String
|
let act = sample["activityType"] as? String
|
||||||
Task { @MainActor [weak self] in
|
Task { @MainActor [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
let wasNotLive = self.status != .live
|
||||||
if let v = hr { self.heartRate = v }
|
if let v = hr { self.heartRate = v }
|
||||||
if let v = kcal { self.activeEnergyKcal = v }
|
if let v = kcal { self.activeEnergyKcal = v }
|
||||||
if let v = dist { self.distanceMeters = v }
|
if let v = dist { self.distanceMeters = v }
|
||||||
@@ -58,8 +76,35 @@ final class LiveStore: ObservableObject {
|
|||||||
if let v = act { self.activityType = v }
|
if let v = act { self.activityType = v }
|
||||||
self.lastSampleAt = Date()
|
self.lastSampleAt = Date()
|
||||||
self.status = .live
|
self.status = .live
|
||||||
|
self.lostSince = nil
|
||||||
|
|
||||||
|
// Hook Live Activity / Dynamic Island (iOS 16.1+ uniquement,
|
||||||
|
// l'app cible iOS 15 mais ActivityKit n'existe qu'à partir de 16.1)
|
||||||
|
if #available(iOS 16.2, *) {
|
||||||
|
let state = self.currentState(isLive: true)
|
||||||
|
if wasNotLive {
|
||||||
|
LiveActivityManager.shared.start(
|
||||||
|
label: self.activityLabel ?? "Séance",
|
||||||
|
symbol: self.activitySymbol,
|
||||||
|
state: state
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LiveActivityManager.shared.update(state: state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 16.2, *)
|
||||||
|
private func currentState(isLive: Bool) -> CoachLiveActivityAttributes.ContentState {
|
||||||
|
CoachLiveActivityAttributes.ContentState(
|
||||||
|
heartRate: heartRate,
|
||||||
|
activeEnergyKcal: activeEnergyKcal,
|
||||||
|
distanceMeters: distanceMeters,
|
||||||
|
elapsedSec: elapsedSec,
|
||||||
|
isLive: isLive
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Formatted accessors (utilisés par CoachLiveView)
|
// MARK: - Formatted accessors (utilisés par CoachLiveView)
|
||||||
|
|
||||||
@@ -95,4 +140,15 @@ final class LiveStore: ObservableObject {
|
|||||||
default: return nil
|
default: return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// SF Symbol pour l'activité (utilisé par la Live Activity).
|
||||||
|
var activitySymbol: String {
|
||||||
|
switch activityType {
|
||||||
|
case "cycling": return "bicycle"
|
||||||
|
case "walking": return "figure.walk"
|
||||||
|
case "hiking": return "figure.walk"
|
||||||
|
case "strength": return "dumbbell.fill"
|
||||||
|
case "mobility": return "figure.yoga"
|
||||||
|
default: return "figure.run"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
ios/App/CoachLiveActivity/CoachLiveActivityAttributes.swift
Normal file
23
ios/App/CoachLiveActivity/CoachLiveActivityAttributes.swift
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// CoachLiveActivityAttributes.swift
|
||||||
|
// Modèle partagé App ↔ Widget Extension (membre des 2 targets).
|
||||||
|
// Définit ce que la Live Activity / Dynamic Island affiche.
|
||||||
|
|
||||||
|
import ActivityKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@available(iOS 16.2, *)
|
||||||
|
struct CoachLiveActivityAttributes: ActivityAttributes {
|
||||||
|
/// Données dynamiques (mises à jour à chaque sample reçu de la Watch).
|
||||||
|
public struct ContentState: Codable, Hashable {
|
||||||
|
var heartRate: Double
|
||||||
|
var activeEnergyKcal: Double
|
||||||
|
var distanceMeters: Double
|
||||||
|
var elapsedSec: TimeInterval
|
||||||
|
var isLive: Bool // true = direct ; false = perdu/en pause
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Données fixes (set au start, non modifiables).
|
||||||
|
var activityLabel: String // "Course", "Vélo", "Marche", "Renfo", ou "Séance"
|
||||||
|
var activitySymbol: String // SF Symbol (figure.run, bicycle, figure.walk, dumbbell)
|
||||||
|
var startedAt: Date
|
||||||
|
}
|
||||||
13
ios/App/CoachLiveActivity/CoachLiveActivityBundle.swift
Normal file
13
ios/App/CoachLiveActivity/CoachLiveActivityBundle.swift
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// CoachLiveActivityBundle.swift
|
||||||
|
// Entry point @main de l'extension WidgetKit. Contient le ou les Widgets
|
||||||
|
// exposés par cette extension. Ici juste la Live Activity du live workout.
|
||||||
|
|
||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct CoachLiveActivityBundle: WidgetBundle {
|
||||||
|
var body: some Widget {
|
||||||
|
CoachLiveActivityWidget()
|
||||||
|
}
|
||||||
|
}
|
||||||
158
ios/App/CoachLiveActivity/CoachLiveActivityWidget.swift
Normal file
158
ios/App/CoachLiveActivity/CoachLiveActivityWidget.swift
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
// CoachLiveActivityWidget.swift
|
||||||
|
// Définition Widget pour la Live Activity (Lock Screen + Dynamic Island).
|
||||||
|
// Structure validée sur le template Apple Xcode 26.5 (Widget Extension /
|
||||||
|
// LiveActivity.swift) + SDK swiftinterface WidgetKit iOS 26.5
|
||||||
|
// (DynamicIsland init signature : expanded:compactLeading:compactTrailing:minimal:).
|
||||||
|
|
||||||
|
import ActivityKit
|
||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CoachLiveActivityWidget: Widget {
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
ActivityConfiguration(for: CoachLiveActivityAttributes.self) { context in
|
||||||
|
// ─── Lock screen / banner ─────────────────────────────────────
|
||||||
|
lockScreenView(context: context)
|
||||||
|
.activityBackgroundTint(Color.black.opacity(0.55))
|
||||||
|
.activitySystemActionForegroundColor(.white)
|
||||||
|
|
||||||
|
} dynamicIsland: { context in
|
||||||
|
DynamicIsland {
|
||||||
|
// Expanded UI (long-press l'île)
|
||||||
|
DynamicIslandExpandedRegion(.leading) {
|
||||||
|
metricExpanded(label: "FC",
|
||||||
|
value: hrText(context.state),
|
||||||
|
color: .red,
|
||||||
|
symbol: "heart.fill")
|
||||||
|
}
|
||||||
|
DynamicIslandExpandedRegion(.trailing) {
|
||||||
|
metricExpanded(label: "Kcal",
|
||||||
|
value: kcalText(context.state),
|
||||||
|
color: .green,
|
||||||
|
symbol: "flame.fill")
|
||||||
|
}
|
||||||
|
DynamicIslandExpandedRegion(.center) {
|
||||||
|
Label(context.attributes.activityLabel,
|
||||||
|
systemImage: context.attributes.activitySymbol)
|
||||||
|
.font(.caption2.weight(.semibold))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
DynamicIslandExpandedRegion(.bottom) {
|
||||||
|
HStack(spacing: 18) {
|
||||||
|
metricInline(label: "Distance",
|
||||||
|
value: distKmText(context.state),
|
||||||
|
unit: "km",
|
||||||
|
color: .orange)
|
||||||
|
Divider().frame(height: 28)
|
||||||
|
metricInline(label: "Durée",
|
||||||
|
value: elapsedText(context.state.elapsedSec),
|
||||||
|
unit: "",
|
||||||
|
color: .white)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
} compactLeading: {
|
||||||
|
HStack(spacing: 3) {
|
||||||
|
Image(systemName: "heart.fill")
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
Text(hrText(context.state))
|
||||||
|
.font(.caption2.weight(.bold).monospacedDigit())
|
||||||
|
}
|
||||||
|
} compactTrailing: {
|
||||||
|
Text(elapsedText(context.state.elapsedSec))
|
||||||
|
.font(.caption2.weight(.semibold).monospacedDigit())
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
} minimal: {
|
||||||
|
Image(systemName: "heart.fill")
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
.symbolEffect(.pulse, isActive: context.state.isLive)
|
||||||
|
}
|
||||||
|
.keylineTint(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Lock screen layout
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func lockScreenView(context: ActivityViewContext<CoachLiveActivityAttributes>) -> some View {
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: context.attributes.activitySymbol)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
Text(context.attributes.activityLabel)
|
||||||
|
.font(.headline)
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "heart.fill")
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
.symbolEffect(.pulse, isActive: context.state.isLive)
|
||||||
|
Text(context.state.isLive ? "En direct" : "Connexion perdue")
|
||||||
|
.font(.caption.weight(.semibold))
|
||||||
|
.foregroundStyle(context.state.isLive ? .green : .orange)
|
||||||
|
}
|
||||||
|
HStack(spacing: 14) {
|
||||||
|
metricBlock(value: hrText(context.state), unit: "bpm", color: .red)
|
||||||
|
metricBlock(value: kcalText(context.state), unit: "kcal", color: .green)
|
||||||
|
metricBlock(value: distKmText(context.state), unit: "km", color: .orange)
|
||||||
|
metricBlock(value: elapsedText(context.state.elapsedSec), unit: "", color: .white)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 14)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func metricBlock(value: String, unit: String, color: Color) -> some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Text(value)
|
||||||
|
.font(.system(.title3, design: .rounded).weight(.heavy).monospacedDigit())
|
||||||
|
.foregroundStyle(color)
|
||||||
|
if !unit.isEmpty {
|
||||||
|
Text(unit).font(.caption2).foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func metricExpanded(label: String, value: String, color: Color, symbol: String) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Label(label, systemImage: symbol)
|
||||||
|
.font(.caption2.weight(.semibold))
|
||||||
|
.foregroundStyle(color)
|
||||||
|
Text(value)
|
||||||
|
.font(.title2.weight(.heavy).monospacedDigit())
|
||||||
|
.foregroundStyle(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func metricInline(label: String, value: String, unit: String, color: Color) -> some View {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Text(label).font(.caption2).foregroundStyle(.secondary)
|
||||||
|
Text(value).font(.callout.weight(.bold).monospacedDigit()).foregroundStyle(color)
|
||||||
|
if !unit.isEmpty {
|
||||||
|
Text(unit).font(.caption2).foregroundStyle(.tertiary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Formatters (dupliqués ici car non-import de LiveStore depuis l'extension)
|
||||||
|
|
||||||
|
private func hrText(_ s: CoachLiveActivityAttributes.ContentState) -> String {
|
||||||
|
s.heartRate > 0 ? "\(Int(s.heartRate.rounded()))" : "—"
|
||||||
|
}
|
||||||
|
private func kcalText(_ s: CoachLiveActivityAttributes.ContentState) -> String {
|
||||||
|
s.activeEnergyKcal > 0 ? "\(Int(s.activeEnergyKcal.rounded()))" : "—"
|
||||||
|
}
|
||||||
|
private func distKmText(_ s: CoachLiveActivityAttributes.ContentState) -> String {
|
||||||
|
s.distanceMeters > 0 ? String(format: "%.2f", s.distanceMeters / 1000) : "—"
|
||||||
|
}
|
||||||
|
private func elapsedText(_ sec: TimeInterval) -> String {
|
||||||
|
let total = Int(sec)
|
||||||
|
let h = total / 3600
|
||||||
|
let m = (total % 3600) / 60
|
||||||
|
let s = total % 60
|
||||||
|
if h > 0 { return "\(h):\(String(format: "%02d", m)):\(String(format: "%02d", s))" }
|
||||||
|
return "\(m):\(String(format: "%02d", s))"
|
||||||
|
}
|
||||||
|
}
|
||||||
27
ios/App/CoachLiveActivity/Info.plist
Normal file
27
ios/App/CoachLiveActivity/Info.plist
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Coach Live</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(MARKETING_VERSION)</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.widgetkit-extension</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
Reference in New Issue
Block a user