// // SessionStatusLiveLiveActivity.swift import ActivityKit import SwiftUI import WidgetKit struct SessionActivityAttributes: ActivityAttributes, Sendable { public struct ContentState: Codable, Hashable, Sendable { var elapsed: TimeInterval var totalAttempts: Int var completedProblems: Int } var gymName: String var startTime: Date } struct SessionStatusLiveLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: SessionActivityAttributes.self) { context in LiveActivityView(context: context) .activityBackgroundTint(Color.blue.opacity(0.2)) .activitySystemActionForegroundColor(Color.primary) } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { VStack(alignment: .leading, spacing: 4) { if let uiImage = UIImage(named: "AppIcon") { Image(uiImage: uiImage) .resizable() .frame(width: 24, height: 24) .clipShape(RoundedRectangle(cornerRadius: 6)) } else { Image(systemName: "figure.climbing") .font(.title3) .foregroundColor(.accentColor) } Text(context.attributes.gymName) .font(.caption2) .foregroundColor(.secondary) .lineLimit(1) } .padding(.leading, 8) } DynamicIslandExpandedRegion(.trailing) { VStack(alignment: .trailing, spacing: 4) { LiveTimerView(start: context.attributes.startTime) .font(.title2) .fontWeight(.bold) .monospacedDigit() HStack(spacing: 4) { Image(systemName: "hand.raised.fill") .foregroundColor(.green) .font(.caption) Text("\(context.state.totalAttempts)") .font(.caption) .fontWeight(.semibold) Text("attempts") .font(.caption2) .foregroundColor(.secondary) } } .padding(.trailing, 8) } DynamicIslandExpandedRegion(.bottom) { HStack { Text("\(context.state.completedProblems) completed") .font(.caption2) .foregroundColor(.secondary) Spacer() Text("Tap to open") .font(.caption2) .foregroundColor(.secondary) } .padding(.horizontal, 12) .padding(.bottom, 4) } } compactLeading: { Image(systemName: "figure.climbing") .font(.footnote) .foregroundColor(.accentColor) } compactTrailing: { LiveTimerView(start: context.attributes.startTime, compact: true) } minimal: { Image(systemName: "figure.climbing") .font(.system(size: 8)) .foregroundColor(.accentColor) } } } } struct LiveActivityView: View { let context: ActivityViewContext var body: some View { HStack(spacing: 16) { LiveTimerView(start: context.attributes.startTime) .font(.largeTitle) .fontWeight(.bold) .monospacedDigit() .frame(minWidth: 80) VStack(alignment: .leading, spacing: 12) { HStack(spacing: 6) { if let uiImage = UIImage(named: "AppIcon") { Image(uiImage: uiImage) .resizable() .frame(width: 28, height: 28) .clipShape(RoundedRectangle(cornerRadius: 7)) } else { Image(systemName: "figure.climbing") .font(.title2) .foregroundColor(.accentColor) } VStack(alignment: .leading, spacing: 0) { Text(context.attributes.gymName) .font(.headline) .fontWeight(.semibold) .lineLimit(1) Text("Climbing Session") .font(.caption) .foregroundColor(.secondary) } } HStack(spacing: 20) { VStack(alignment: .leading, spacing: 4) { HStack(spacing: 6) { Image(systemName: "hand.raised.fill") .foregroundColor(.green) .font(.title3) Text("\(context.state.totalAttempts)") .font(.title2) .fontWeight(.bold) } Text("Total Attempts") .font(.caption) .foregroundColor(.secondary) } VStack(alignment: .leading, spacing: 4) { HStack(spacing: 6) { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) .font(.title3) Text("\(context.state.completedProblems)") .font(.title2) .fontWeight(.bold) } Text("Completed") .font(.caption) .foregroundColor(.secondary) } Spacer() } } Spacer() } .padding(.horizontal, 16) .padding(.vertical, 12) } } struct LiveTimerView: View { let start: Date let compact: Bool let minimal: Bool init(start: Date, compact: Bool = false, minimal: Bool = false) { self.start = start self.compact = compact self.minimal = minimal } var body: some View { if minimal { Text(timerInterval: start...Date.distantFuture, countsDown: false) .font(.system(size: 8, weight: .medium, design: .monospaced)) } else if compact { Text(timerInterval: start...Date.distantFuture, countsDown: false) .font(.caption.monospacedDigit()) .frame(maxWidth: 40) .minimumScaleFactor(0.7) } else { Text(timerInterval: start...Date.distantFuture, countsDown: false) .monospacedDigit() } } } // Alias for compatibility typealias TimerView = LiveTimerView extension SessionActivityAttributes { fileprivate static var preview: SessionActivityAttributes { SessionActivityAttributes( gymName: "Summit Climbing Gym", startTime: Date().addingTimeInterval(-1234)) } } extension SessionActivityAttributes.ContentState { fileprivate static var active: SessionActivityAttributes.ContentState { SessionActivityAttributes.ContentState( elapsed: 1234, totalAttempts: 8, completedProblems: 2) } fileprivate static var busy: SessionActivityAttributes.ContentState { SessionActivityAttributes.ContentState( elapsed: 3600, totalAttempts: 25, completedProblems: 7) } } #Preview("Notification", as: .content, using: SessionActivityAttributes.preview) { SessionStatusLiveLiveActivity() } contentStates: { SessionActivityAttributes.ContentState.active SessionActivityAttributes.ContentState.busy }