← Back to index

WatchContentView

Spots Watch App
CodeWhat It DoesHow It Does It
▶ IMPORTS
import CoreLocation import SwiftUIFramework importsImports CoreLocation, SwiftUI.
struct WatchContentView: View { @EnvironmentObject var store: WatchEntryStore @State private var sortByNearest = false // MARK: - Distance helpers private func distanceText(for entry: WatchEntry) -> String? { guard let userLoc = store.userLocation, let lat = entry.latitude, let lon = entry.longitude else { return nil } let meters = userLoc.distance(from: CLLocation(latitude: lat, longitude: lon)) let miles = meters / 1609.34 if miles < 0.1 { return "nearby" } if miles < 10 { return String(format: "%.1f mi", miles) } return String(format: "%.0f mi", miles) } private func distanceMeters(for entry: WatchEntry) -> Double { guard let userLoc = store.userLocation, let lat = entry.latitude, let lon = entry.longitude else { return .greatestFiniteMagnitude } return userLoc.distance(from: CLLocation(latitude: lat, longitude: lon)) } // MARK: - Default sort (priority / star groups) var toTryByPriority: [(priority: Int, entries: [WatchEntry])] { [5, 4, 3, 2, 1, 0].compactMap { priority in let group = store.entries .filter { !$0.beenThere && $0.rating == priority } .sorted { $0.playName.lowercased() < $1.playName.lowercased() } return group.isEmpty ? nil : (priority: priority, entries: group) } } var beenByStarRating: [(starRating: Int, entries: [WatchEntry])] { [5, 4, 3, 2, 1, 0].compactMap { stars in let group = store.entries .filter { $0.beenThere && $0.starRating == stars } .sorted { $0.playName.lowercased() < $1.playName.lowercased() } return group.isEmpty ? nil : (starRating: stars, entries: group) } } // MARK: - Nearest sort (flat lists sorted by distance) var toTryByDistance: [WatchEntry] { store.entries .filter { !$0.beenThere } .sorted { distanceMeters(for: $0) < distanceMeters(for: $1) } } var beenByDistance: [WatchEntry] { store.entries .filter { $0.beenThere } .sorted { distanceMeters(for: $0) < distanceMeters(for: $1) } } @State private var syncMessage: String? = nil var body: some View { if !store.isLoaded { // Cache hasn't been read yet — show nothing to avoid a flash of the empty state Color.clear } else if store.entries.isEmpty { VStack(spacing: 6) { Image(systemName: "fork.knife") .font(.system(size: 28)) .foregroundColor(.orange) Text("Nothing Here") .font(.headline) if let msg = syncMessage { Text(msg) .font(.caption2) .foregroundColor(.secondary) .multilineTextAlignment(.center) } else { Text("Open Spots on your iPhone") .font(.caption2) .foregroundColor(.secondary) .multilineTextAlignment(.center) } Button(action: { let sent = store.requestSync() syncMessage = sent ? "Requesting…" : "Open Spots on iPhone first" DispatchQueue.main.asyncAfter(deadline: .now() + 3) { syncMessage = nil } }) { Label("Refresh", systemImage: "arrow.clockwise") .font(.caption2) } .buttonStyle(.borderless) .padding(.top, 4) } .navigationTitle("Spots") } else { VStack(spacing: 0) { ZStack { Text("SPOTS") .font(.system(size: 14, weight: .semibold)) .foregroundColor(lavender) .frame(maxWidth: .infinity, alignment: .center) .allowsHitTesting(false) HStack { Button(action: { sortByNearest.toggle() if sortByNearest { store.startLocationIfNeeded() } }) { Image(systemName: sortByNearest ? "location.fill" : "location") .font(.system(size: 16)) .foregroundColor(sortByNearest ? Color(red: 1, green: 0.388, blue: 0.278) : .white) } .buttonStyle(.borderless) .padding(.leading, 30) Spacer() } } .padding(.horizontal, 8) .padding(.vertical, 4) List { if sortByNearest { let allByDistance = store.entries .sorted { distanceMeters(for: $0) < distanceMeters(for: $1) } ForEach(allByDistance) { entry in NavigationLink(destination: WatchEntryDetailView(entry: entry)) { WatchEntryRowView(entry: entry, showAsBeen: entry.beenThere, distanceText: distanceText(for: entry)) } } } else { if !toTryByPriority.isEmpty { Section("Try") { ForEach(Array(toTryByPriority.enumerated()), id: \.element.priority) { idx, group in HStack(spacing: 3) { ForEach(1...5, id: \.self) { dot in Image(systemName: dot <= group.priority ? "circle.fill" : "circle") .font(.system(size: 8)) .foregroundColor(dot <= group.priority ? .white : .gray.opacity(0.3)) } } .frame(height: 14) .listRowBackground(Color.clear) .listRowInsets(EdgeInsets(top: idx == 0 ? 2 : 4, leading: 8, bottom: 0, trailing: 8)) ForEach(group.entries) { entry in NavigationLink(destination: WatchEntryDetailView(entry: entry)) { WatchEntryRowView(entry: entry, showAsBeen: false, showPriority: false) } } } } } if !beenByStarRating.isEmpty { Section("Been") { ForEach(Array(beenByStarRating.enumerated()), id: \.element.starRating) { idx, group in HStack(spacing: 3) { ForEach(1...5, id: \.self) { star in Image(systemName: star <= group.starRating ? "star.fill" : "star") .font(.system(size: 8)) .foregroundColor(star <= group.starRating ? .yellow : .gray.opacity(0.3)) } } .frame(height: 14) .listRowBackground(Color.clear) .listRowInsets(EdgeInsets(top: idx == 0 ? 2 : 4, leading: 8, bottom: 0, trailing: 8)) ForEach(group.entries) { entry in NavigationLink(destination: WatchEntryDetailView(entry: entry)) { WatchEntryRowView(entry: entry, showAsBeen: true, showPriority: false) } } } } } } } .listStyle(.plain) .environment(\.defaultMinListRowHeight, 0) } .navigationTitle("") .toolbar(.hidden, for: .navigationBar) .ignoresSafeArea(edges: .top) } } }`WatchContentView` structDefines the `WatchContentView` struct. Conforms to View.