import ActivityKit import SwiftUI import WidgetKit @main struct ClimbingActivityWidgetBundle: WidgetBundle { var body: some Widget { ClimbingActivityWidget() } } struct ClimbingActivityWidget: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: SessionActivityAttributes.self) { context in // Lock Screen/Banner UI goes here LiveActivityView(context: context) .activityBackgroundTint(Color.cyan) .activitySystemActionForegroundColor(Color.black) } dynamicIsland: { context in DynamicIsland { // Expanded UI goes here DynamicIslandExpandedRegion(.leading) { Text("🧗‍♂️") .font(.title2) } DynamicIslandExpandedRegion(.trailing) { Text(context.attributes.gymName) .lineLimit(1) .font(.caption) } DynamicIslandExpandedRegion(.bottom) { HStack { Label("\(context.state.totalAttempts)", systemImage: "flame") Spacer() Label("\(context.state.completedProblems)", systemImage: "checkmark.circle") Spacer() TimerView(start: context.attributes.startTime) } .font(.caption) } } compactLeading: { Text("🧗‍♂️") } compactTrailing: { Text("\(context.state.totalAttempts)") .monospacedDigit() } minimal: { Text("🧗‍♂️") } } } } struct LiveActivityView: View { let context: ActivityViewContext var body: some View { VStack(alignment: .leading, spacing: 8) { HStack { Text("🧗‍♂️ \(context.attributes.gymName)") .font(.headline) .lineLimit(1) Spacer() TimerView(start: context.attributes.startTime) .font(.subheadline) .foregroundColor(.secondary) } HStack(spacing: 20) { VStack(alignment: .leading, spacing: 2) { Text("Attempts") .font(.caption) .foregroundColor(.secondary) HStack { Image(systemName: "flame.fill") .foregroundColor(.orange) Text("\(context.state.totalAttempts)") .font(.title3) .fontWeight(.semibold) } } VStack(alignment: .leading, spacing: 2) { Text("Completed") .font(.caption) .foregroundColor(.secondary) HStack { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) Text("\(context.state.completedProblems)") .font(.title3) .fontWeight(.semibold) } } Spacer() } } .padding(.horizontal, 16) .padding(.vertical, 12) } } struct TimerView: View { let start: Date @State private var now = Date() private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() var body: some View { Text(formatElapsed(from: start, to: now)) .monospacedDigit() .onReceive(timer) { time in now = time } } private func formatElapsed(from: Date, to: Date) -> String { let interval = Int(to.timeIntervalSince(from)) let hours = interval / 3600 let minutes = (interval % 3600) / 60 let seconds = interval % 60 if hours > 0 { return String(format: "%02d:%02d:%02d", hours, minutes, seconds) } else { return String(format: "%02d:%02d", minutes, seconds) } } } // Preview for development #Preview("Live Activity", as: .content, using: SessionActivityAttributes.preview) { ClimbingActivityWidget() } contentStates: { SessionActivityAttributes.ContentState(elapsed: 1234, totalAttempts: 13, completedProblems: 4) SessionActivityAttributes.ContentState(elapsed: 2400, totalAttempts: 25, completedProblems: 8) } extension SessionActivityAttributes { static var preview: SessionActivityAttributes { SessionActivityAttributes(gymName: "Summit Climbing", startTime: Date()) } }