Phase 4.A.1: Live Activity + Dynamic Island für laufende Gassi-Tour
Neues Widget-Extension-Target BanYaroGoWidgetExtension: - Bundle-ID app.banyaro.ios.BanYaroGoWidget - NSExtensionPointIdentifier = com.apple.widgetkit-extension - Synced root group + explizite Info.plist + Embed-Phase in App-Target - Cross-Membership der Shared/WalkActivityAttributes.swift in beiden Targets Shared/WalkActivityAttributes.swift: - ActivityAttributes mit startedAt (fix) - ContentState mit distanceMeters, elapsedSeconds, pointCount, isPaused, isAutoPaused BanYaroGoWidget/WalkLiveActivity.swift: - Lock-Screen-View: Pfote-Icon + Status-Pille (Live/Pause/Auto-Pause) + Stats-Spalten (Distanz/Dauer/Punkte) - Dynamic Island compact: Pfote leading, Distanz trailing - Dynamic Island minimal: nur Pfote - Dynamic Island expanded: Distanz/Dauer/Status mit Pfote zentriert WalkActivityController: - @MainActor Facade um ActivityKit - start() prüft areActivitiesEnabled, killt orphaned current, request mit Initial-State - update() async via Task - end() mit dismissalPolicy.immediate TrackingView: - .onChange(of: tracker.isTracking) → Start/End - persistTicker (5s) → update - .onChange(of: tracker.isPaused/isAutoPaused) → sofort update für saubere UX BanYaroGo-Info.plist: NSSupportsLiveActivities = true
This commit is contained in:
parent
fec7c79b05
commit
500a645bfd
8 changed files with 458 additions and 0 deletions
|
|
@ -58,5 +58,7 @@
|
||||||
<array>
|
<array>
|
||||||
<string>location</string>
|
<string>location</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>NSSupportsLiveActivities</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,46 @@
|
||||||
objectVersion = 70;
|
objectVersion = 70;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
BB0000000000000000000010 /* WalkActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0000000000000000000001 /* WalkActivityAttributes.swift */; };
|
||||||
|
BB0000000000000000000011 /* WalkActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0000000000000000000001 /* WalkActivityAttributes.swift */; };
|
||||||
|
BB0000000000000000000012 /* BanYaroGoWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = BB0000000000000000000005 /* BanYaroGoWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
BB0000000000000000000041 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = AA0000000000000000000001 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = BB0000000000000000000020;
|
||||||
|
remoteInfo = BanYaroGoWidgetExtension;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
BB0000000000000000000024 /* Embed Foundation Extensions */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 13;
|
||||||
|
files = (
|
||||||
|
BB0000000000000000000012 /* BanYaroGoWidgetExtension.appex in Embed Foundation Extensions */,
|
||||||
|
);
|
||||||
|
name = "Embed Foundation Extensions";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
AA0000000000000000000005 /* BanYaroGo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BanYaroGo.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
AA0000000000000000000005 /* BanYaroGo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BanYaroGo.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
BB0000000000000000000001 /* WalkActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalkActivityAttributes.swift; sourceTree = "<group>"; };
|
||||||
|
BB0000000000000000000003 /* BanYaroGoWidget-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "BanYaroGoWidget-Info.plist"; sourceTree = "<group>"; };
|
||||||
|
BB0000000000000000000005 /* BanYaroGoWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = BanYaroGoWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
AA0000000000000000000006 /* BanYaroGo */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = BanYaroGo; sourceTree = "<group>"; };
|
AA0000000000000000000006 /* BanYaroGo */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = BanYaroGo; sourceTree = "<group>"; };
|
||||||
|
BB0000000000000000000002 /* BanYaroGoWidget */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = BanYaroGoWidget; sourceTree = "<group>"; };
|
||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
|
@ -22,13 +56,23 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
BB0000000000000000000022 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
AA0000000000000000000002 = {
|
AA0000000000000000000002 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
BB0000000000000000000006 /* Shared */,
|
||||||
AA0000000000000000000006 /* BanYaroGo */,
|
AA0000000000000000000006 /* BanYaroGo */,
|
||||||
|
BB0000000000000000000002 /* BanYaroGoWidget */,
|
||||||
|
BB0000000000000000000003 /* BanYaroGoWidget-Info.plist */,
|
||||||
AA0000000000000000000003 /* Products */,
|
AA0000000000000000000003 /* Products */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -37,10 +81,19 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
AA0000000000000000000005 /* BanYaroGo.app */,
|
AA0000000000000000000005 /* BanYaroGo.app */,
|
||||||
|
BB0000000000000000000005 /* BanYaroGoWidgetExtension.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
BB0000000000000000000006 /* Shared */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
BB0000000000000000000001 /* WalkActivityAttributes.swift */,
|
||||||
|
);
|
||||||
|
path = Shared;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
|
@ -51,10 +104,12 @@
|
||||||
AA000000000000000000000D /* Sources */,
|
AA000000000000000000000D /* Sources */,
|
||||||
AA000000000000000000000E /* Frameworks */,
|
AA000000000000000000000E /* Frameworks */,
|
||||||
AA000000000000000000000F /* Resources */,
|
AA000000000000000000000F /* Resources */,
|
||||||
|
BB0000000000000000000024 /* Embed Foundation Extensions */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
|
BB0000000000000000000040 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
fileSystemSynchronizedGroups = (
|
fileSystemSynchronizedGroups = (
|
||||||
AA0000000000000000000006 /* BanYaroGo */,
|
AA0000000000000000000006 /* BanYaroGo */,
|
||||||
|
|
@ -64,6 +119,26 @@
|
||||||
productReference = AA0000000000000000000005 /* BanYaroGo.app */;
|
productReference = AA0000000000000000000005 /* BanYaroGo.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
|
BB0000000000000000000020 /* BanYaroGoWidgetExtension */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = BB0000000000000000000030 /* Build configuration list for PBXNativeTarget "BanYaroGoWidgetExtension" */;
|
||||||
|
buildPhases = (
|
||||||
|
BB0000000000000000000021 /* Sources */,
|
||||||
|
BB0000000000000000000022 /* Frameworks */,
|
||||||
|
BB0000000000000000000023 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
BB0000000000000000000002 /* BanYaroGoWidget */,
|
||||||
|
);
|
||||||
|
name = BanYaroGoWidgetExtension;
|
||||||
|
productName = BanYaroGoWidgetExtension;
|
||||||
|
productReference = BB0000000000000000000005 /* BanYaroGoWidgetExtension.appex */;
|
||||||
|
productType = "com.apple.product-type.app-extension";
|
||||||
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
|
|
@ -77,6 +152,9 @@
|
||||||
AA0000000000000000000004 = {
|
AA0000000000000000000004 = {
|
||||||
CreatedOnToolsVersion = 26.5;
|
CreatedOnToolsVersion = 26.5;
|
||||||
};
|
};
|
||||||
|
BB0000000000000000000020 = {
|
||||||
|
CreatedOnToolsVersion = 26.5;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
buildConfigurationList = AA0000000000000000000007 /* Build configuration list for PBXProject "BanYaroGo" */;
|
buildConfigurationList = AA0000000000000000000007 /* Build configuration list for PBXProject "BanYaroGo" */;
|
||||||
|
|
@ -93,6 +171,7 @@
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
AA0000000000000000000004 /* BanYaroGo */,
|
AA0000000000000000000004 /* BanYaroGo */,
|
||||||
|
BB0000000000000000000020 /* BanYaroGoWidgetExtension */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
@ -105,6 +184,13 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
BB0000000000000000000023 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
|
@ -112,11 +198,28 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
BB0000000000000000000010 /* WalkActivityAttributes.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
BB0000000000000000000021 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
BB0000000000000000000011 /* WalkActivityAttributes.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
BB0000000000000000000040 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = BB0000000000000000000020 /* BanYaroGoWidgetExtension */;
|
||||||
|
targetProxy = BB0000000000000000000041 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
AA0000000000000000000009 /* Debug */ = {
|
AA0000000000000000000009 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
|
@ -285,6 +388,55 @@
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
BB0000000000000000000031 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = H436BR6YWX;
|
||||||
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
|
INFOPLIST_FILE = "BanYaroGoWidget-Info.plist";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = app.banyaro.ios.BanYaroGoWidget;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
BB0000000000000000000032 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = H436BR6YWX;
|
||||||
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
|
INFOPLIST_FILE = "BanYaroGoWidget-Info.plist";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = app.banyaro.ios.BanYaroGoWidget;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
|
|
@ -306,6 +458,15 @@
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
BB0000000000000000000030 /* Build configuration list for PBXNativeTarget "BanYaroGoWidgetExtension" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
BB0000000000000000000031 /* Debug */,
|
||||||
|
BB0000000000000000000032 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
};
|
};
|
||||||
rootObject = AA0000000000000000000001 /* Project object */;
|
rootObject = AA0000000000000000000001 /* Project object */;
|
||||||
|
|
|
||||||
41
BanYaroGo/Tracking/WalkActivityController.swift
Normal file
41
BanYaroGo/Tracking/WalkActivityController.swift
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import Foundation
|
||||||
|
import ActivityKit
|
||||||
|
|
||||||
|
/// Tiny facade around ActivityKit so TrackingView doesn't have to know the
|
||||||
|
/// details. There is at most one walk activity alive at any time.
|
||||||
|
@MainActor
|
||||||
|
enum WalkActivityController {
|
||||||
|
private static var current: Activity<WalkActivityAttributes>?
|
||||||
|
|
||||||
|
static func start(startedAt: Date, initialState: WalkActivityAttributes.WalkActivityState) {
|
||||||
|
guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
|
||||||
|
// Kill anything left over from a previous (orphaned) session.
|
||||||
|
if current != nil { end() }
|
||||||
|
let attributes = WalkActivityAttributes(startedAt: startedAt)
|
||||||
|
let content = ActivityContent(state: initialState, staleDate: nil)
|
||||||
|
do {
|
||||||
|
current = try Activity.request(
|
||||||
|
attributes: attributes,
|
||||||
|
content: content,
|
||||||
|
pushType: nil
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
print("Live Activity request failed: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func update(_ state: WalkActivityAttributes.WalkActivityState) {
|
||||||
|
let activity = current
|
||||||
|
Task { @MainActor in
|
||||||
|
await activity?.update(ActivityContent(state: state, staleDate: nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func end() {
|
||||||
|
let activity = current
|
||||||
|
current = nil
|
||||||
|
Task { @MainActor in
|
||||||
|
await activity?.end(nil, dismissalPolicy: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,7 +34,17 @@ struct TrackingView: View {
|
||||||
.onReceive(persistTicker) { _ in
|
.onReceive(persistTicker) { _ in
|
||||||
tracker.checkAutoPause()
|
tracker.checkAutoPause()
|
||||||
persistActive()
|
persistActive()
|
||||||
|
updateLiveActivity()
|
||||||
}
|
}
|
||||||
|
.onChange(of: tracker.isTracking) { _, isTracking in
|
||||||
|
if isTracking {
|
||||||
|
startLiveActivity()
|
||||||
|
} else {
|
||||||
|
WalkActivityController.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: tracker.isPaused) { _, _ in updateLiveActivity() }
|
||||||
|
.onChange(of: tracker.isAutoPaused) { _, _ in updateLiveActivity() }
|
||||||
.onAppear { offerResumeIfNeeded() }
|
.onAppear { offerResumeIfNeeded() }
|
||||||
.sheet(isPresented: $showFinishSheet) {
|
.sheet(isPresented: $showFinishSheet) {
|
||||||
FinishWalkSheet(
|
FinishWalkSheet(
|
||||||
|
|
@ -320,6 +330,29 @@ struct TrackingView: View {
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private func startLiveActivity() {
|
||||||
|
guard let startedAt = tracker.startedAt else { return }
|
||||||
|
WalkActivityController.start(
|
||||||
|
startedAt: startedAt,
|
||||||
|
initialState: liveActivityState()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateLiveActivity() {
|
||||||
|
guard tracker.isTracking else { return }
|
||||||
|
WalkActivityController.update(liveActivityState())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func liveActivityState() -> WalkActivityAttributes.WalkActivityState {
|
||||||
|
WalkActivityAttributes.WalkActivityState(
|
||||||
|
distanceMeters: tracker.totalDistanceMeters,
|
||||||
|
elapsedSeconds: tracker.effectiveElapsedSeconds,
|
||||||
|
pointCount: tracker.points.count,
|
||||||
|
isPaused: tracker.isPaused,
|
||||||
|
isAutoPaused: tracker.isAutoPaused
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private func formatDuration(_ seconds: Int) -> String {
|
private func formatDuration(_ seconds: Int) -> String {
|
||||||
let h = seconds / 3600
|
let h = seconds / 3600
|
||||||
let m = (seconds % 3600) / 60
|
let m = (seconds % 3600) / 60
|
||||||
|
|
|
||||||
29
BanYaroGoWidget-Info.plist
Normal file
29
BanYaroGoWidget-Info.plist
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Ban Yaro Go</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>
|
||||||
9
BanYaroGoWidget/BanYaroGoWidgetBundle.swift
Normal file
9
BanYaroGoWidget/BanYaroGoWidgetBundle.swift
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct BanYaroGoWidgetBundle: WidgetBundle {
|
||||||
|
var body: some Widget {
|
||||||
|
WalkLiveActivity()
|
||||||
|
}
|
||||||
|
}
|
||||||
145
BanYaroGoWidget/WalkLiveActivity.swift
Normal file
145
BanYaroGoWidget/WalkLiveActivity.swift
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
import ActivityKit
|
||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct WalkLiveActivity: Widget {
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
ActivityConfiguration(for: WalkActivityAttributes.self) { context in
|
||||||
|
// Lock-Screen / Banner view
|
||||||
|
lockScreen(state: context.state)
|
||||||
|
.activityBackgroundTint(Color.accentColor.opacity(0.12))
|
||||||
|
.activitySystemActionForegroundColor(Color.accentColor)
|
||||||
|
} dynamicIsland: { context in
|
||||||
|
DynamicIsland {
|
||||||
|
DynamicIslandExpandedRegion(.leading) {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Label("Distanz", systemImage: "ruler")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Text(distanceLabel(context.state.distanceMeters))
|
||||||
|
.font(.title3.bold().monospacedDigit())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicIslandExpandedRegion(.trailing) {
|
||||||
|
VStack(alignment: .trailing) {
|
||||||
|
Label("Dauer", systemImage: "clock")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Text(durationLabel(context.state.elapsedSeconds))
|
||||||
|
.font(.title3.bold().monospacedDigit())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicIslandExpandedRegion(.center) {
|
||||||
|
Image(systemName: pawIcon(for: context.state))
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundStyle(Color.accentColor)
|
||||||
|
}
|
||||||
|
DynamicIslandExpandedRegion(.bottom) {
|
||||||
|
if context.state.isPaused {
|
||||||
|
statusPill("Pause", color: .orange)
|
||||||
|
} else if context.state.isAutoPaused {
|
||||||
|
statusPill("Auto-Pause", color: .gray)
|
||||||
|
} else {
|
||||||
|
Text("\(context.state.pointCount) Punkte aufgezeichnet")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} compactLeading: {
|
||||||
|
Image(systemName: pawIcon(for: context.state))
|
||||||
|
.foregroundStyle(Color.accentColor)
|
||||||
|
} compactTrailing: {
|
||||||
|
Text(distanceLabel(context.state.distanceMeters))
|
||||||
|
.font(.caption2.monospacedDigit())
|
||||||
|
} minimal: {
|
||||||
|
Image(systemName: pawIcon(for: context.state))
|
||||||
|
.foregroundStyle(Color.accentColor)
|
||||||
|
}
|
||||||
|
.widgetURL(URL(string: "banyarogo://walk"))
|
||||||
|
.keylineTint(Color.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func lockScreen(state: WalkActivityAttributes.WalkActivityState) -> some View {
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
Image(systemName: pawIcon(for: state))
|
||||||
|
.font(.system(size: 36))
|
||||||
|
.foregroundStyle(Color.accentColor)
|
||||||
|
.frame(width: 56, height: 56)
|
||||||
|
.background(Color.accentColor.opacity(0.15), in: Circle())
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Text("Ban Yaro Go")
|
||||||
|
.font(.headline)
|
||||||
|
if state.isPaused {
|
||||||
|
statusPill("Pause", color: .orange)
|
||||||
|
} else if state.isAutoPaused {
|
||||||
|
statusPill("Auto-Pause", color: .gray)
|
||||||
|
} else {
|
||||||
|
statusPill("Live", color: .green)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
statColumn(
|
||||||
|
value: distanceLabel(state.distanceMeters),
|
||||||
|
label: "Distanz"
|
||||||
|
)
|
||||||
|
Divider().frame(height: 28)
|
||||||
|
statColumn(
|
||||||
|
value: durationLabel(state.elapsedSeconds),
|
||||||
|
label: "Dauer"
|
||||||
|
)
|
||||||
|
Divider().frame(height: 28)
|
||||||
|
statColumn(
|
||||||
|
value: "\(state.pointCount)",
|
||||||
|
label: "Punkte"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func statColumn(value: String, label: String) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(value)
|
||||||
|
.font(.subheadline.bold().monospacedDigit())
|
||||||
|
Text(label)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func statusPill(_ text: String, color: Color) -> some View {
|
||||||
|
Text(text)
|
||||||
|
.font(.caption2.bold())
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
.background(color, in: Capsule())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pawIcon(for state: WalkActivityAttributes.WalkActivityState) -> String {
|
||||||
|
if state.isPaused || state.isAutoPaused {
|
||||||
|
return "pause.circle.fill"
|
||||||
|
}
|
||||||
|
return "pawprint.fill"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func distanceLabel(_ meters: Double) -> String {
|
||||||
|
if meters >= 1000 {
|
||||||
|
return String(format: "%.2f km", meters / 1000)
|
||||||
|
}
|
||||||
|
return "\(Int(meters)) m"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func durationLabel(_ seconds: Int) -> String {
|
||||||
|
let h = seconds / 3600
|
||||||
|
let m = (seconds % 3600) / 60
|
||||||
|
let s = seconds % 60
|
||||||
|
if h > 0 { return String(format: "%d:%02d:%02d", h, m, s) }
|
||||||
|
return String(format: "%d:%02d", m, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
38
Shared/WalkActivityAttributes.swift
Normal file
38
Shared/WalkActivityAttributes.swift
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import Foundation
|
||||||
|
import ActivityKit
|
||||||
|
|
||||||
|
/// Live Activity attributes for a running Gassi-Tour. Lives in `Shared/` and
|
||||||
|
/// is compiled into both the app target (which starts/updates/ends activities)
|
||||||
|
/// and the widget extension (which renders the lock screen + Dynamic Island).
|
||||||
|
struct WalkActivityAttributes: ActivityAttributes {
|
||||||
|
public typealias ContentState = WalkActivityState
|
||||||
|
|
||||||
|
public struct WalkActivityState: Codable, Hashable {
|
||||||
|
public var distanceMeters: Double
|
||||||
|
public var elapsedSeconds: Int
|
||||||
|
public var pointCount: Int
|
||||||
|
public var isPaused: Bool
|
||||||
|
public var isAutoPaused: Bool
|
||||||
|
|
||||||
|
public init(
|
||||||
|
distanceMeters: Double,
|
||||||
|
elapsedSeconds: Int,
|
||||||
|
pointCount: Int,
|
||||||
|
isPaused: Bool,
|
||||||
|
isAutoPaused: Bool
|
||||||
|
) {
|
||||||
|
self.distanceMeters = distanceMeters
|
||||||
|
self.elapsedSeconds = elapsedSeconds
|
||||||
|
self.pointCount = pointCount
|
||||||
|
self.isPaused = isPaused
|
||||||
|
self.isAutoPaused = isAutoPaused
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk start time — fixed for the duration of the activity.
|
||||||
|
public var startedAt: Date
|
||||||
|
|
||||||
|
public init(startedAt: Date) {
|
||||||
|
self.startedAt = startedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue