← Back to index

AddEntryView

Spots
CodeWhat It DoesHow It Does It
▶ IMPORTS
import SwiftUI import UniformTypeIdentifiers import PhotosUIFramework importsImports SwiftUI, UniformTypeIdentifiers, PhotosUI.
struct AddEntryView: View { @Environment(\.dismiss) var dismiss @Binding var entries: [SpotEntry] var onSave: ((UUID) -> Void)? = nil var mode: EntryMode = .food @State private var playName = "" @State private var dateTime: Date = { var components = Calendar.current.dateComponents([.year, .month, .day], from: Date()) components.hour = 19 components.minute = 0 return Calendar.current.date(from: components) ?? Date() }() @State private var cuisine = "" @State private var address = "" @State private var phone = "" @State private var website = "" @State private var hours = "" @State private var neighborhood = "" @State private var confirmationLink = "" @State private var isTargeted = false @State private var selectedPhoto: PhotosPickerItem? @State private var imageData: Data? @State private var imageURL = "" @State private var isLoadingImage = false @State private var remindersEnabled = false @State private var notes = "" @State private var rating = 0 @State private var beenThere = false @State private var starRating = 0 @State private var showDatePicker = false @State private var consider = false var body: some View { NavigationStack { Form { if mode == .show || mode == .event { Section(header: Text(mode == .show ? "Show Information" : "Event Information").font(.system(size: 12))) { LabeledContent("Show") { TextField("Name", text: $playName) .font(.system(size: 15)) .multilineTextAlignment(.trailing) } .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) LabeledContent("Date & Time") { DatePicker("", selection: $dateTime, displayedComponents: [.date, .hourAndMinute]) .labelsHidden() .opacity(consider ? 0 : 1) .disabled(consider) } .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) LabeledContent("Venue") { TextField("", text: $cuisine) .font(.system(size: 15)) .multilineTextAlignment(.trailing) } .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) LabeledContent("Address") { TextField("", text: $address) .font(.system(size: 15)) .multilineTextAlignment(.trailing) } .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) LabeledContent("Phone") { TextField("", text: $phone) .font(.system(size: 15)) .multilineTextAlignment(.trailing) .keyboardType(.phonePad) } .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) LabeledContent("Website") { TextField("", text: $website) .font(.system(size: 15)) .multilineTextAlignment(.trailing) .keyboardType(.URL) .textInputAutocapitalization(.never) .autocorrectionDisabled() } .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) LabeledContent("Row / Seat") { TextField("", text: $neighborhood) .font(.system(size: 15)) .multilineTextAlignment(.trailing) } .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) HStack { Button { consider.toggle() if consider { var c = Calendar.current.dateComponents([.year, .month, .day], from: Date()) c.hour = 19; c.minute = 0 dateTime = Calendar.current.date(from: c) ?? Date() } } label: { Text("Consider") .font(.system(size: 14)) .padding(.horizontal, 12) .padding(.vertical, 6) .background(consider ? Color.orange : Color(.systemGray5)) .foregroundColor(consider ? .white : .primary) .clipShape(RoundedRectangle(cornerRadius: 8)) } .buttonStyle(.borderless) Spacer() HStack(spacing: 4) { ForEach(1...5, id: \.self) { star in Button { starRating = (starRating == star) ? 0 : star } label: { Image(systemName: star <= starRating ? "star.fill" : "star") .font(.system(size: 18)) .foregroundColor(star <= starRating ? .yellow : .gray.opacity(0.4)) } .buttonStyle(.borderless) } } .id(starRating) } .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) } } else { Section(header: Text(modeSectionHeader).font(.system(size: 12))) { TextField("Name", text: $playName) .font(.system(size: 15)) .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) TextField(modeCuisineLabel, text: $cuisine) .font(.system(size: 15)) .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) TextField("Neighborhood", text: $neighborhood) .font(.system(size: 15)) .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) TextField("Address", text: $address) .font(.system(size: 15)) .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) TextField("Phone", text: $phone) .font(.system(size: 15)) .keyboardType(.phonePad) .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) TextField("Website", text: $website) .font(.system(size: 15)) .keyboardType(.URL) .textInputAutocapitalization(.never) .autocorrectionDisabled() .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) TextField("Hours", text: $hours) .font(.system(size: 15)) .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) LabeledContent("Priority") { HStack(spacing: 4) { ForEach(1...5, id: \.self) { dot in Image(systemName: dot <= rating ? "circle.fill" : "circle") .font(.system(size: 14)) .foregroundColor(dot <= rating ? .primary : .gray.opacity(0.4)) .onTapGesture { rating = (rating == dot) ? 0 : dot } } } } .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) HStack { Text("Been").font(.system(size: 15)) Spacer() Toggle("", isOn: $beenThere).labelsHidden() } .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) if beenThere { HStack { Text("Rating").font(.system(size: 15)) Spacer() HStack(spacing: 4) { ForEach(1...5, id: \.self) { star in Button { starRating = (starRating == star) ? star - 1 : star } label: { Image(systemName: star <= starRating ? "star.fill" : "star") .font(.system(size: 14)) .foregroundColor(star <= starRating ? .yellow : .gray.opacity(0.4)) }.buttonStyle(.borderless) } }.id(starRating) } .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) } } } Section(header: Text("Image").font(.system(size: 12))) { HStack { TextField("Image URL", text: $imageURL) .font(.system(size: 15)) .textInputAutocapitalization(.never) .autocorrectionDisabled() if isLoadingImage { ProgressView() .scaleEffect(0.7) } else if !imageURL.isEmpty { Button("Load") { loadImageFromURL() } .font(.system(size: 14)) } } .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) PhotosPicker(selection: $selectedPhoto, matching: .images) { HStack { if let imageData = imageData, let uiImage = UIImage(data: imageData) { Image(uiImage: uiImage) .resizable() .scaledToFill() .frame(width: 50, height: 67) .clipped() .cornerRadius(4) Text("Change Image") .font(.system(size: 15)) } else { Image(systemName: "photo") .font(.system(size: 15)) Text("Select from Photos") .font(.system(size: 15)) } } } .onChange(of: selectedPhoto) { _, newValue in Task { if let data = try? await newValue?.loadTransferable(type: Data.self) { imageData = data } } } .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) } if mode != .show && mode != .event { Section(header: Text("Calendar").font(.system(size: 12))) { if showDatePicker { DatePicker("Date & Time", selection: $dateTime, displayedComponents: [.date, .hourAndMinute]) .font(.system(size: 15)) .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) } else { Button(action: { showDatePicker = true }) { HStack { Image(systemName: "calendar.badge.plus") .font(.system(size: 14)) .foregroundColor(.blue) Text("Add to Calendar") .font(.system(size: 15)) .foregroundColor(.blue) Spacer() } } .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) } } } Section(header: Text("More Info").font(.system(size: 12))) { TextField("More Info Link", text: $confirmationLink) .font(.system(size: 15)) .textInputAutocapitalization(.never) .background(isTargeted ? Color.blue.opacity(0.1) : Color.clear) .onDrop(of: [.url, .text], isTargeted: $isTargeted) { providers in handleDrop(providers: providers) } .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) } Section(header: Text("Reminders").font(.system(size: 12))) { Toggle(isOn: $remindersEnabled) { HStack { Image(systemName: "bell.fill") .font(.system(size: 14)) .foregroundColor(remindersEnabled ? .orange : .gray) Text("Enable Reminders") .font(.system(size: 15)) } } .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) if remindersEnabled { Text("Reminders: 1 week, 1 day, 6 hours, 1 hour before.") .font(.system(size: 13)) .foregroundColor(.secondary) .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) } } Section(header: Text("Notes").font(.system(size: 12))) { TextField("Add notes...", text: $notes, axis: .vertical) .font(.system(size: 15)) .lineLimit(3...6) .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) } } .environment(\.defaultMinListRowHeight, 0) .navigationTitle("Add") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } .font(.system(size: 16)) } ToolbarItem(placement: .confirmationAction) { Button("Save") { let newEntry = SpotEntry( playName: playName, dateTime: dateTime, cuisine: cuisine, address: address, phone: phone, website: website, neighborhood: neighborhood, hours: hours, confirmationLink: confirmationLink, imageData: imageData.map { EntryStore.resizedImageData($0, maxDimension: 1200) }, remindersEnabled: remindersEnabled, notes: notes, rating: rating, beenThere: beenThere, starRating: (mode == .show || mode == .event) ? starRating : (beenThere ? starRating : 0), hasCustomDate: (mode == .show || mode == .event) ? !consider : showDatePicker, entryMode: mode, consider: (mode == .show || mode == .event) ? consider : false ) entries.append(newEntry) NotificationManager.shared.scheduleReminders(for: newEntry) onSave?(newEntry.id) dismiss() } .font(.system(size: 16)) .disabled(playName.isEmpty) } } } } // MARK: - Mode helpers private var modeSectionHeader: String { switch mode { case .food: return String(localized: "Restaurant Information") case .show: return String(localized: "Show Information") case .event: return String(localized: "Event Information") case .place: return String(localized: "Place Information") case .shop: return String(localized: "Shop Information") } } private var modeCuisineLabel: String { switch mode { case .food: return String(localized: "Cuisine") case .show: return String(localized: "Venue") case .event: return String(localized: "Venue") case .place: return String(localized: "Category") case .shop: return String(localized: "Category") } } func handleDrop(providers: [NSItemProvider]) -> Bool { guard let provider = providers.first else { return false } if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) { provider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { item, error in if let data = item as? Data, let url = URL(dataRepresentation: data, relativeTo: nil) { DispatchQueue.main.async { confirmationLink = url.absoluteString } } } return true } else if provider.hasItemConformingToTypeIdentifier(UTType.text.identifier) { provider.loadItem(forTypeIdentifier: UTType.text.identifier, options: nil) { item, error in if let text = item as? String { DispatchQueue.main.async { confirmationLink = text } } } return true } return false } func loadImageFromURL() { guard let url = URL(string: imageURL) else { return } isLoadingImage = true Task { do { let (data, _) = try await URLSession.shared.data(from: url) await MainActor.run { imageData = data isLoadingImage = false } } catch { await MainActor.run { isLoadingImage = false } } } } }`AddEntryView` structDefines the `AddEntryView` struct. Conforms to View.