| Code | What It Does | How It Does It |
| ▶ IMPORTS | | |
| import WidgetKit
import SwiftUI | Framework imports | Imports WidgetKit, SwiftUI. |
| ▶ APP GROUP KEY (MUST MATCH WATCHENTRYSTORE) | | |
| private let appGroupID = "group.com.spots.shared"
private let entriesKey = "watchEntries" | `appGroupID` let | Property `appGroupID`. |
| ▶ TIMELINE ENTRY | | |
| struct EntryEntry: TimelineEntry {
let date: Date
let playName: String
let cuisine: String
let neighborhood: String
let toTryCount: Int // total "to try" entries in the list
// Placeholder / empty state shown in the widget gallery
static let placeholder = EntryEntry(
date: Date(),
playName: "Katz's",
cuisine: "Delicatessen",
neighborhood: "Lower East Side",
toTryCount: 12
)
} | `EntryEntry` struct | Defines the `EntryEntry` struct. Conforms to TimelineEntry. |
| ▶ TIMELINE PROVIDER | | |
| struct EntryTimelineProvider: TimelineProvider {
func placeholder(in context: Context) -> EntryEntry {
.placeholder
}
func getSnapshot(in context: Context, completion: @escaping (EntryEntry) -> Void) {
completion(topEntry() ?? .placeholder)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<EntryEntry>) -> Void) {
let entry = topEntry() ?? .placeholder
// Refresh every 30 minutes — entries change infrequently
let nextRefresh = Calendar.current.date(byAdding: .minute, value: 30, to: Date()) ?? Date()
let timeline = Timeline(entries: [entry], policy: .after(nextRefresh))
completion(timeline)
}
// MARK: - Helpers
/// Returns the highest-priority "to try" entry (not yet visited).
/// Sorts by rating descending, then alphabetically. Falls back to any entry if all are "been".
private func topEntry() -> EntryEntry? {
guard
let defaults = UserDefaults(suiteName: appGroupID),
let data = defaults.data(forKey: entriesKey),
let entries = try? JSONDecoder().decode([WatchEntry].self, from: data),
!entries.isEmpty
else { return nil }
let toTry = entries.filter { !$0.beenThere }
let pool = toTry.isEmpty ? entries : toTry // fall back to all if none left to try
// Highest rating first, then A–Z
let top = pool.sorted {
if $0.rating != $1.rating { return $0.rating > $1.rating }
return $0.playName.lowercased() < $1.playName.lowercased()
}.first!
return EntryEntry(
date: Date(),
playName: top.playName,
cuisine: top.cuisine,
neighborhood: top.neighborhood,
toTryCount: toTry.count
)
}
} | `EntryTimelineProvider` struct | Defines the `EntryTimelineProvider` struct. Conforms to TimelineProvider. |
| ▶ COMPLICATION VIEWS | | |
| /// Circular: app icon only
struct CircularComplicationView: View {
let entry: EntryEntry
var body: some View {
ZStack {
AccessoryWidgetBackground()
Image("SpotsIcon")
.renderingMode(.original)
.resizable()
.scaledToFit()
.clipShape(Circle())
.padding(4)
}
}
} | Documentation comment | Describes the following declaration. |
| /// Rectangular: spot name + cuisine or neighborhood
struct RectangularComplicationView: View {
let entry: EntryEntry
var body: some View {
VStack(alignment: .leading, spacing: 2) {
Text(entry.playName)
.font(.system(size: 13, weight: .semibold))
.lineLimit(1)
HStack {
let detail = entry.cuisine.isEmpty ? entry.neighborhood : entry.cuisine
if !detail.isEmpty {
Text(detail)
.font(.system(size: 12))
.foregroundColor(.secondary)
.lineLimit(1)
}
Spacer()
if entry.toTryCount > 0 {
Text("\(entry.toTryCount) to try")
.font(.system(size: 11))
.foregroundColor(.secondary)
}
}
}
.padding(.horizontal, 2)
}
} | Documentation comment | Describes the following declaration. |
| /// Inline: single line — top spot name
struct InlineComplicationView: View {
let entry: EntryEntry
var body: some View {
let detail = entry.cuisine.isEmpty ? entry.neighborhood : entry.cuisine
if detail.isEmpty {
Text(entry.playName)
} else {
Text("\(entry.playName) · \(detail)")
}
}
} | Documentation comment | Describes the following declaration. |
| ▶ WIDGET CONFIGURATION | | |
| struct SpotsComplication: Widget {
let kind = "SpotsComplication"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: EntryTimelineProvider()) { entry in
SpotsComplicationEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Spots")
.description("Shows your top-priority spot to try next.")
.supportedFamilies([
.accessoryCircular,
.accessoryRectangular,
.accessoryInline
])
}
} | `SpotsComplication` struct | Defines the `SpotsComplication` struct. Conforms to Widget. |
| /// Root dispatcher — picks the right view for each complication family
struct SpotsComplicationEntryView: View {
@Environment(\.widgetFamily) var family
let entry: EntryEntry
var body: some View {
switch family {
case .accessoryCircular:
CircularComplicationView(entry: entry)
case .accessoryRectangular:
RectangularComplicationView(entry: entry)
case .accessoryInline:
InlineComplicationView(entry: entry)
default:
CircularComplicationView(entry: entry)
}
}
} | Documentation comment | Describes the following declaration. |