300 lines
8.7 KiB
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: []
|
|
)
|
|
}
|