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>
|
||||
<string>location</string>
|
||||
</array>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -6,12 +6,46 @@
|
|||
objectVersion = 70;
|
||||
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 */
|
||||
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 */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
AA0000000000000000000006 /* BanYaroGo */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = BanYaroGo; sourceTree = "<group>"; };
|
||||
BB0000000000000000000002 /* BanYaroGoWidget */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = BanYaroGoWidget; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
|
@ -22,13 +56,23 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BB0000000000000000000022 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
AA0000000000000000000002 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BB0000000000000000000006 /* Shared */,
|
||||
AA0000000000000000000006 /* BanYaroGo */,
|
||||
BB0000000000000000000002 /* BanYaroGoWidget */,
|
||||
BB0000000000000000000003 /* BanYaroGoWidget-Info.plist */,
|
||||
AA0000000000000000000003 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -37,10 +81,19 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
AA0000000000000000000005 /* BanYaroGo.app */,
|
||||
BB0000000000000000000005 /* BanYaroGoWidgetExtension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BB0000000000000000000006 /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BB0000000000000000000001 /* WalkActivityAttributes.swift */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
|
|
@ -51,10 +104,12 @@
|
|||
AA000000000000000000000D /* Sources */,
|
||||
AA000000000000000000000E /* Frameworks */,
|
||||
AA000000000000000000000F /* Resources */,
|
||||
BB0000000000000000000024 /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
BB0000000000000000000040 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
AA0000000000000000000006 /* BanYaroGo */,
|
||||
|
|
@ -64,6 +119,26 @@
|
|||
productReference = AA0000000000000000000005 /* BanYaroGo.app */;
|
||||
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 */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
|
|
@ -77,6 +152,9 @@
|
|||
AA0000000000000000000004 = {
|
||||
CreatedOnToolsVersion = 26.5;
|
||||
};
|
||||
BB0000000000000000000020 = {
|
||||
CreatedOnToolsVersion = 26.5;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = AA0000000000000000000007 /* Build configuration list for PBXProject "BanYaroGo" */;
|
||||
|
|
@ -93,6 +171,7 @@
|
|||
projectRoot = "";
|
||||
targets = (
|
||||
AA0000000000000000000004 /* BanYaroGo */,
|
||||
BB0000000000000000000020 /* BanYaroGoWidgetExtension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
|
@ -105,6 +184,13 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BB0000000000000000000023 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
|
|
@ -112,11 +198,28 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BB0000000000000000000010 /* WalkActivityAttributes.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BB0000000000000000000021 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BB0000000000000000000011 /* WalkActivityAttributes.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
BB0000000000000000000040 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = BB0000000000000000000020 /* BanYaroGoWidgetExtension */;
|
||||
targetProxy = BB0000000000000000000041 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
AA0000000000000000000009 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
|
|
@ -285,6 +388,55 @@
|
|||
};
|
||||
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 */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
|
|
@ -306,6 +458,15 @@
|
|||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
BB0000000000000000000030 /* Build configuration list for PBXNativeTarget "BanYaroGoWidgetExtension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
BB0000000000000000000031 /* Debug */,
|
||||
BB0000000000000000000032 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
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
|
||||
tracker.checkAutoPause()
|
||||
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() }
|
||||
.sheet(isPresented: $showFinishSheet) {
|
||||
FinishWalkSheet(
|
||||
|
|
@ -320,6 +330,29 @@ struct TrackingView: View {
|
|||
|
||||
// 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 {
|
||||
let h = seconds / 3600
|
||||
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