import WidgetKit import SwiftUI import CoreData // MARK: - Widget Entry struct InvestmentWidgetEntry: TimelineEntry { let date: Date let totalValue: Decimal let dayChange: Decimal let dayChangePercentage: Double let topSources: [(name: String, value: Decimal, color: String)] let sparklineData: [Decimal] } // MARK: - Widget Provider struct InvestmentWidgetProvider: TimelineProvider { func placeholder(in context: Context) -> InvestmentWidgetEntry { InvestmentWidgetEntry( date: Date(), totalValue: 50000, dayChange: 250, dayChangePercentage: 0.5, topSources: [ ("Stocks", 30000, "#10B981"), ("Bonds", 15000, "#3B82F6"), ("Real Estate", 5000, "#F59E0B") ], sparklineData: [45000, 46000, 47000, 48000, 49000, 50000] ) } func getSnapshot(in context: Context, completion: @escaping (InvestmentWidgetEntry) -> Void) { let entry = fetchData() completion(entry) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { let entry = fetchData() // Refresh every hour let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: Date()) ?? Date() let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) completion(timeline) } private func fetchData() -> InvestmentWidgetEntry { let container = CoreDataStack.createWidgetContainer() let context = container.viewContext // Fetch sources let sourceRequest: NSFetchRequest = InvestmentSource.fetchRequest() let sources = (try? context.fetch(sourceRequest)) ?? [] // Calculate total value let totalValue = sources.reduce(Decimal.zero) { $0 + $1.latestValue } // Get top sources let topSources = sources .sorted { $0.latestValue > $1.latestValue } .prefix(3) .map { (name: $0.name, value: $0.latestValue, color: $0.category?.colorHex ?? "#3B82F6") } // Calculate day change (simplified - would need proper historical data) let dayChange: Decimal = 0 let dayChangePercentage: Double = 0 // Get sparkline data (last 6 data points) let sparklineData: [Decimal] = [] return InvestmentWidgetEntry( date: Date(), totalValue: totalValue, dayChange: dayChange, dayChangePercentage: dayChangePercentage, topSources: Array(topSources), sparklineData: sparklineData ) } } // MARK: - Small Widget View struct SmallWidgetView: View { let entry: InvestmentWidgetEntry var body: some View { VStack(alignment: .leading, spacing: 8) { Text("Portfolio") .font(.caption) .foregroundColor(.secondary) Text(entry.totalValue.compactCurrencyString) .font(.title2.weight(.bold)) .minimumScaleFactor(0.7) .lineLimit(1) Spacer() HStack(spacing: 4) { Image(systemName: entry.dayChange >= 0 ? "arrow.up.right" : "arrow.down.right") .font(.caption2) Text(String(format: "%.1f%%", entry.dayChangePercentage)) .font(.caption.weight(.medium)) } .foregroundColor(entry.dayChange >= 0 ? .green : .red) } .padding() .containerBackground(.background, for: .widget) } } // MARK: - Medium Widget View struct MediumWidgetView: View { let entry: InvestmentWidgetEntry var body: some View { HStack(spacing: 16) { // Left side - Total value VStack(alignment: .leading, spacing: 8) { Text("Portfolio") .font(.caption) .foregroundColor(.secondary) Text(entry.totalValue.compactCurrencyString) .font(.title.weight(.bold)) .minimumScaleFactor(0.7) .lineLimit(1) HStack(spacing: 4) { Image(systemName: entry.dayChange >= 0 ? "arrow.up.right" : "arrow.down.right") .font(.caption2) Text(entry.dayChange.compactCurrencyString) .font(.caption.weight(.medium)) Text("(\(String(format: "%.1f%%", entry.dayChangePercentage)))") .font(.caption) .foregroundColor(.secondary) } .foregroundColor(entry.dayChange >= 0 ? .green : .red) } Spacer() // Right side - Top sources VStack(alignment: .trailing, spacing: 6) { ForEach(entry.topSources, id: \.name) { source in HStack(spacing: 6) { Text(source.name) .font(.caption) .foregroundColor(.secondary) .lineLimit(1) Text(source.value.shortCurrencyString) .font(.caption.weight(.medium)) } } } } .padding() .containerBackground(.background, for: .widget) } } // MARK: - Accessory Circular View (Lock Screen) struct AccessoryCircularView: View { let entry: InvestmentWidgetEntry var body: some View { ZStack { AccessoryWidgetBackground() VStack(spacing: 2) { Image(systemName: entry.dayChange >= 0 ? "arrow.up.right" : "arrow.down.right") .font(.caption) Text(String(format: "%.1f%%", entry.dayChangePercentage)) .font(.caption2.weight(.semibold)) } } .containerBackground(.background, for: .widget) } } // MARK: - Accessory Rectangular View (Lock Screen) struct AccessoryRectangularView: View { let entry: InvestmentWidgetEntry var body: some View { VStack(alignment: .leading, spacing: 2) { Text("Portfolio") .font(.caption2) .foregroundColor(.secondary) Text(entry.totalValue.compactCurrencyString) .font(.headline) HStack(spacing: 4) { Image(systemName: entry.dayChange >= 0 ? "arrow.up.right" : "arrow.down.right") .font(.caption2) Text(String(format: "%.1f%%", entry.dayChangePercentage)) .font(.caption2) } .foregroundColor(entry.dayChange >= 0 ? .green : .red) } .containerBackground(.background, for: .widget) } } // MARK: - Widget Configuration struct InvestmentWidget: Widget { let kind: String = "InvestmentWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: InvestmentWidgetProvider()) { entry in InvestmentWidgetEntryView(entry: entry) } .configurationDisplayName("Portfolio Value") .description("View your total investment portfolio value.") .supportedFamilies([ .systemSmall, .systemMedium, .accessoryCircular, .accessoryRectangular ]) } } // MARK: - Widget Entry View struct InvestmentWidgetEntryView: View { @Environment(\.widgetFamily) var family let entry: InvestmentWidgetEntry var body: some View { switch family { case .systemSmall: SmallWidgetView(entry: entry) case .systemMedium: MediumWidgetView(entry: entry) case .accessoryCircular: AccessoryCircularView(entry: entry) case .accessoryRectangular: AccessoryRectangularView(entry: entry) default: SmallWidgetView(entry: entry) } } } // MARK: - Widget Bundle @main struct InvestmentTrackerWidgetBundle: WidgetBundle { var body: some Widget { InvestmentWidget() } } // MARK: - Previews #Preview("Small", as: .systemSmall) { InvestmentWidget() } timeline: { InvestmentWidgetEntry( date: Date(), totalValue: 50000, dayChange: 250, dayChangePercentage: 0.5, topSources: [], sparklineData: [] ) } #Preview("Medium", as: .systemMedium) { InvestmentWidget() } timeline: { InvestmentWidgetEntry( date: Date(), totalValue: 50000, dayChange: 250, dayChangePercentage: 0.5, topSources: [ ("Stocks", 30000, "#10B981"), ("Bonds", 15000, "#3B82F6"), ("Real Estate", 5000, "#F59E0B") ], sparklineData: [] ) }