InvestmentTrackerApp/InvestmentTrackerWidget/InvestmentWidget.swift

300 lines
8.7 KiB
Swift

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<InvestmentWidgetEntry>) -> 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> = 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: []
)
}