import ActivityKit import SwiftUI import WidgetKit // MARK: - Widget configuration struct DownloadLiveActivityWidget: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: DownloadActivityAttributes.self) { context in // Lock Screen / Notification Center banner DownloadLockScreenView(context: context) .activityBackgroundTint(Color(.systemBackground).opacity(0.6)) } dynamicIsland: { context in DynamicIsland { // Expanded region shown when the user long-presses the island DynamicIslandExpandedRegion(.leading) { Image(systemName: "arrow.down.circle.fill") .foregroundStyle(.green) .font(.title2) } DynamicIslandExpandedRegion(.center) { Text(context.attributes.itemTitle) .font(.caption.bold()) .lineLimit(1) .minimumScaleFactor(0.8) } DynamicIslandExpandedRegion(.bottom) { VStack(spacing: 4) { ProgressView(value: context.state.progress) .tint(.green) HStack { Text(bytesFormatted(context.state.bytesDownloaded)) .font(.caption2) .foregroundStyle(.secondary) .contentTransition(.numericText()) Spacer() Text(percentFormatted(context.state.progress)) .font(.caption2.bold()) .monospacedDigit() .contentTransition(.numericText()) } } } } compactLeading: { Image(systemName: "arrow.down.circle.fill") .foregroundStyle(.green) .font(.caption) } compactTrailing: { Text(percentFormatted(context.state.progress)) .font(.caption2.bold()) .monospacedDigit() .contentTransition(.numericText()) } minimal: { Image(systemName: "arrow.down.circle.fill") .foregroundStyle(.green) .font(.caption2) } } } } // MARK: - Lock Screen view struct DownloadLockScreenView: View { let context: ActivityViewContext var body: some View { VStack(alignment: .leading, spacing: 8) { HStack(spacing: 8) { Image(systemName: "arrow.down.circle.fill") .foregroundStyle(.green) Text(context.attributes.itemTitle) .font(.subheadline.bold()) .lineLimit(1) Spacer() Text(percentFormatted(context.state.progress)) .font(.subheadline.bold()) .monospacedDigit() .contentTransition(.numericText()) } ProgressView(value: context.state.progress) .tint(.green) Text(bytesFormatted(context.state.bytesDownloaded) + " heruntergeladen") .font(.caption) .foregroundStyle(.secondary) .contentTransition(.numericText()) } .padding() } } // MARK: - Helpers private func percentFormatted(_ fraction: Double) -> String { String(format: "%.0f%%", fraction * 100) } private func bytesFormatted(_ bytes: Int64) -> String { let mb = Double(bytes) / (1_024 * 1_024) if mb >= 1 { return String(format: "%.1f MB", mb) } let kb = Double(bytes) / 1_024 if kb >= 1 { return String(format: "%.0f KB", kb) } return "\(bytes) B" } // MARK: - Previews private let previewAttributes = DownloadActivityAttributes(itemTitle: "The Name of the Wind") #Preview("Lock Screen", as: .content, using: previewAttributes) { DownloadLiveActivityWidget() } contentStates: { DownloadActivityAttributes.ContentState(progress: 0.35, bytesDownloaded: 52_428_800) DownloadActivityAttributes.ContentState(progress: 0.9, bytesDownloaded: 135_266_304) } #Preview("Dynamic Island Expanded", as: .dynamicIsland(.expanded), using: previewAttributes) { DownloadLiveActivityWidget() } contentStates: { DownloadActivityAttributes.ContentState(progress: 0.35, bytesDownloaded: 52_428_800) DownloadActivityAttributes.ContentState(progress: 0.9, bytesDownloaded: 135_266_304) } #Preview("Dynamic Island Compact", as: .dynamicIsland(.compact), using: previewAttributes) { DownloadLiveActivityWidget() } contentStates: { DownloadActivityAttributes.ContentState(progress: 0.35, bytesDownloaded: 52_428_800) DownloadActivityAttributes.ContentState(progress: 0.9, bytesDownloaded: 135_266_304) } #Preview("Dynamic Island Minimal", as: .dynamicIsland(.minimal), using: previewAttributes) { DownloadLiveActivityWidget() } contentStates: { DownloadActivityAttributes.ContentState(progress: 0.35, bytesDownloaded: 52_428_800) }