Files
Sportstime/SportsTime/Features/Home/Views/HomeView.swift
Trey t dc142bd14b feat: expand XCUITest coverage to 54 QA scenarios with accessibility IDs and fix test failures
Add 22 new UI tests across 8 test files covering Home, Schedule, Progress,
Settings, TabNavigation, TripSaving, and TripOptions. Add accessibility
identifiers to 11 view files for test element discovery. Fix sport chip
assertion logic (all sports start selected, tap deselects), scroll container
issues on iOS 26 nested ScrollViews, toggle interaction, and delete trip flow.
Update QA coverage map from 32 to 54 automated test cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 19:44:22 -06:00

853 lines
30 KiB
Swift

//
// HomeView.swift
// SportsTime
//
import SwiftUI
import SwiftData
struct HomeView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.colorScheme) private var colorScheme
@Query(sort: \SavedTrip.updatedAt, order: .reverse) private var savedTrips: [SavedTrip]
@State private var showNewTrip = false
@State private var selectedSport: Sport?
@State private var selectedTab = 0
@State private var suggestedTripsGenerator = SuggestedTripsGenerator()
@State private var selectedSuggestedTrip: SuggestedTrip?
@State private var displayedTips: [PlanningTip] = []
@State private var showProPaywall = false
#if DEBUG
@State private var marketingAutoScroll = false
#endif
var body: some View {
TabView(selection: $selectedTab) {
// Home Tab
NavigationStack {
AdaptiveHomeContent(
showNewTrip: $showNewTrip,
selectedTab: $selectedTab,
selectedSuggestedTrip: $selectedSuggestedTrip,
marketingAutoScroll: marketingAutoScrollBinding,
savedTrips: savedTrips,
suggestedTripsGenerator: suggestedTripsGenerator,
displayedTips: displayedTips
)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
showNewTrip = true
} label: {
Image(systemName: "plus.circle.fill")
.font(.title2)
.foregroundStyle(Theme.warmOrange)
}
.accessibilityLabel("Create new trip")
.accessibilityIdentifier("home.createNewTripButton")
}
}
}
.tabItem {
Label("Home", systemImage: "house.fill")
}
.tag(0)
.accessibilityIdentifier("tab.home")
// Schedule Tab
NavigationStack {
ScheduleListView()
}
.tabItem {
Label("Schedule", systemImage: "calendar")
}
.tag(1)
.accessibilityIdentifier("tab.schedule")
// My Trips Tab
NavigationStack {
SavedTripsListView(trips: savedTrips)
}
.tabItem {
Label("My Trips", systemImage: "suitcase.fill")
}
.tag(2)
.accessibilityIdentifier("tab.myTrips")
// Progress Tab
NavigationStack {
if StoreManager.shared.isPro {
ProgressTabView()
} else {
ProLockedView(feature: .progressTracking) {
showProPaywall = true
}
}
}
.tabItem {
Label("Progress", systemImage: "chart.bar.fill")
}
.tag(3)
.accessibilityIdentifier("tab.progress")
// Settings Tab
NavigationStack {
SettingsView()
}
.tabItem {
Label("Settings", systemImage: "gear")
}
.tag(4)
.accessibilityIdentifier("tab.settings")
}
.tint(Theme.warmOrange)
.onChange(of: selectedTab) { oldTab, newTab in
let tabNames = ["Home", "Schedule", "My Trips", "Progress", "Settings"]
let newName = newTab < tabNames.count ? tabNames[newTab] : "Unknown"
let oldName = oldTab < tabNames.count ? tabNames[oldTab] : nil
AnalyticsManager.shared.track(.tabSwitched(tab: newName, previousTab: oldName))
AnalyticsManager.shared.trackScreen(newName)
#if DEBUG
if newTab == 0 && UserDefaults.standard.bool(forKey: "marketingVideoMode") {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
marketingAutoScroll = true
}
}
#endif
}
.sheet(isPresented: $showNewTrip) {
TripWizardView()
.environment(\.isDemoMode, ProcessInfo.isDemoMode)
}
.onChange(of: showNewTrip) { _, isShowing in
if !isShowing {
selectedSport = nil
}
}
.task {
if suggestedTripsGenerator.suggestedTrips.isEmpty && !suggestedTripsGenerator.isLoading {
await suggestedTripsGenerator.generateTrips()
}
}
.onAppear {
let tabNames = ["Home", "Schedule", "My Trips", "Progress", "Settings"]
let activeTabName = selectedTab < tabNames.count ? tabNames[selectedTab] : "Unknown"
AnalyticsManager.shared.trackScreen(activeTabName)
if displayedTips.isEmpty {
displayedTips = PlanningTips.random(3)
}
}
.sheet(item: $selectedSuggestedTrip) { suggestedTrip in
NavigationStack {
TripDetailView(trip: suggestedTrip.trip)
}
}
.sheet(isPresented: $showProPaywall) {
PaywallView(source: "home_progress_gate")
}
}
private var marketingAutoScrollBinding: Binding<Bool> {
#if DEBUG
return $marketingAutoScroll
#else
return .constant(false)
#endif
}
// MARK: - Hero Card
private var heroCard: some View {
VStack(spacing: Theme.Spacing.lg) {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
Text("Adventure Awaits")
.font(.largeTitle)
.foregroundStyle(Theme.textPrimary(colorScheme))
Text("Plan your ultimate sports road trip. Visit stadiums, catch games, and create unforgettable memories.")
.font(.body)
.foregroundStyle(Theme.textSecondary(colorScheme))
.fixedSize(horizontal: false, vertical: true)
}
.frame(maxWidth: .infinity, alignment: .leading)
Button {
showNewTrip = true
} label: {
HStack(spacing: Theme.Spacing.xs) {
Image(systemName: "map.fill")
Text("Start Planning")
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.padding(Theme.Spacing.md)
.background(Theme.warmOrange)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
}
.pressableStyle()
.glowEffect(color: Theme.warmOrange, radius: 12)
}
.padding(Theme.Spacing.lg)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.xlarge))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.xlarge)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
.shadow(color: Theme.cardShadow(colorScheme), radius: 15, y: 8)
}
// MARK: - Suggested Trips
@ViewBuilder
private var suggestedTripsSection: some View {
if suggestedTripsGenerator.isLoading {
LoadingTripsView(message: suggestedTripsGenerator.loadingMessage)
} else if !suggestedTripsGenerator.suggestedTrips.isEmpty {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
// Header with refresh button
HStack {
Text("Featured Trips")
.font(.title2)
.foregroundStyle(Theme.textPrimary(colorScheme))
Spacer()
Button {
Task {
await suggestedTripsGenerator.refreshTrips()
}
} label: {
Image(systemName: "arrow.clockwise")
.font(.subheadline)
.foregroundStyle(Theme.warmOrange)
}
.minimumHitTarget()
.accessibilityLabel("Refresh trips")
.accessibilityHint("Fetches the latest featured trip recommendations")
}
.padding(.horizontal, Theme.Spacing.md)
// Horizontal carousel grouped by region
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: Theme.Spacing.lg) {
ForEach(suggestedTripsGenerator.tripsByRegion, id: \.region) { regionGroup in
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
// Region header
HStack(spacing: Theme.Spacing.xs) {
Image(systemName: regionGroup.region.iconName)
.font(.caption)
.accessibilityHidden(true)
Text(regionGroup.region.shortName)
.font(.subheadline)
}
.foregroundStyle(Theme.textSecondary(colorScheme))
// Trip cards for this region
HStack(spacing: Theme.Spacing.md) {
ForEach(regionGroup.trips) { suggestedTrip in
Button {
AnalyticsManager.shared.track(.suggestedTripTapped(
region: regionGroup.region.shortName,
stopCount: suggestedTrip.trip.stops.count
))
selectedSuggestedTrip = suggestedTrip
} label: {
SuggestedTripCard(suggestedTrip: suggestedTrip)
}
.buttonStyle(.plain)
}
}
}
}
}
}
.contentMargins(.horizontal, Theme.Spacing.md, for: .scrollContent)
}
} else if let error = suggestedTripsGenerator.error {
// Error state
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
Text("Featured Trips")
.font(.title2)
.foregroundStyle(Theme.textPrimary(colorScheme))
HStack {
Image(systemName: "exclamationmark.triangle")
.foregroundStyle(.orange)
.accessibilityLabel("Error loading trips")
Text(error)
.font(.subheadline)
.foregroundStyle(Theme.textSecondary(colorScheme))
Spacer()
Button("Retry") {
Task {
await suggestedTripsGenerator.generateTrips()
}
}
.font(.subheadline)
.foregroundStyle(Theme.warmOrange)
}
.padding(Theme.Spacing.md)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
}
.padding(.horizontal, Theme.Spacing.md)
}
}
// MARK: - Saved Trips
private var savedTripsSection: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
HStack {
Text("Recent Trips")
.font(.title2)
.foregroundStyle(Theme.textPrimary(colorScheme))
Spacer()
Button {
selectedTab = 2
} label: {
HStack(spacing: 4) {
Text("See All")
Image(systemName: "chevron.right")
.font(.caption)
.accessibilityHidden(true)
}
.font(.subheadline)
.foregroundStyle(Theme.warmOrange)
}
}
ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in
if let trip = savedTrip.trip {
SavedTripCard(savedTrip: savedTrip, trip: trip)
.staggeredAnimation(index: index, delay: 0.05)
}
}
}
}
// MARK: - Tips
private var tipsSection: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
Text("Planning Tips")
.font(.title2)
.foregroundStyle(Theme.textPrimary(colorScheme))
VStack(spacing: Theme.Spacing.xs) {
ForEach(displayedTips) { tip in
TipRow(icon: tip.icon, title: tip.title, subtitle: tip.subtitle)
}
}
.padding(Theme.Spacing.md)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
}
.onAppear {
if displayedTips.isEmpty {
displayedTips = PlanningTips.random(3)
}
}
}
}
// MARK: - Supporting Views
struct SavedTripCard: View {
let savedTrip: SavedTrip
let trip: Trip
@Environment(\.colorScheme) private var colorScheme
var body: some View {
NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games, allowCustomItems: true)
} label: {
HStack(spacing: Theme.Spacing.md) {
// Route preview icon
ZStack {
Circle()
.fill(Theme.warmOrange.opacity(0.15))
.frame(width: 44, height: 44)
Image(systemName: "map.fill")
.foregroundStyle(Theme.warmOrange)
.accessibilityHidden(true)
}
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: 4) {
Text(trip.displayName)
.font(.body)
.foregroundStyle(Theme.textPrimary(colorScheme))
Text(trip.formattedDateRange)
.font(.subheadline)
.foregroundStyle(Theme.textSecondary(colorScheme))
HStack(spacing: Theme.Spacing.sm) {
HStack(spacing: 4) {
Image(systemName: "mappin")
.font(.caption2)
.accessibilityHidden(true)
Text("\(trip.stops.count) cities")
}
HStack(spacing: 4) {
Image(systemName: "sportscourt")
.font(.caption2)
.accessibilityHidden(true)
Text("\(trip.totalGames) games")
}
}
.font(.caption)
.foregroundStyle(Theme.textMuted(colorScheme))
}
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundStyle(Theme.textMuted(colorScheme))
.accessibilityHidden(true)
}
.padding(Theme.Spacing.md)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
.accessibilityElement(children: .combine)
}
.buttonStyle(.plain)
}
}
struct TipRow: View {
let icon: String
let title: String
let subtitle: String
@Environment(\.colorScheme) private var colorScheme
var body: some View {
HStack(spacing: Theme.Spacing.sm) {
ZStack {
Circle()
.fill(Theme.routeGold.opacity(0.15))
.frame(width: 36, height: 36)
Image(systemName: icon)
.font(.subheadline)
.foregroundStyle(Theme.routeGold)
}
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.subheadline)
.foregroundStyle(Theme.textPrimary(colorScheme))
Text(subtitle)
.font(.caption)
.foregroundStyle(Theme.textSecondary(colorScheme))
}
Spacer()
}
}
}
// MARK: - Saved Trips List View
struct SavedTripsListView: View {
let trips: [SavedTrip]
@Environment(\.colorScheme) private var colorScheme
@State private var polls: [TripPoll] = []
@State private var isLoadingPolls = false
@State private var hasLoadedPolls = false
@State private var showCreatePoll = false
@State private var selectedPoll: TripPoll?
#if DEBUG
@State private var showDebugPoll = false
#endif
/// Trips sorted by most cities (stops) first
private var sortedTrips: [SavedTrip] {
trips.sorted { ($0.trip?.stops.count ?? 0) > ($1.trip?.stops.count ?? 0) }
}
/// Trips as domain objects for poll creation
private var tripsForPollCreation: [Trip] {
trips.compactMap { $0.trip }
}
var body: some View {
ScrollView {
LazyVStack(spacing: Theme.Spacing.lg) {
// Polls Section
pollsSection
// Trips Section
tripsSection
}
.padding(Theme.Spacing.md)
}
.themedBackground()
.task {
guard !hasLoadedPolls else { return }
hasLoadedPolls = true
await loadPolls()
}
.refreshable {
await loadPolls()
}
.sheet(isPresented: $showCreatePoll) {
PollCreationView(trips: tripsForPollCreation) { poll in
polls.insert(poll, at: 0)
}
}
.navigationDestination(for: TripPoll.self) { poll in
PollDetailView(pollId: poll.id)
}
#if DEBUG
.sheet(isPresented: $showDebugPoll) {
NavigationStack {
DebugPollPreviewView()
}
}
#endif
}
// MARK: - Polls Section
@ViewBuilder
private var pollsSection: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
HStack {
Text("Group Polls")
.font(.title2)
.foregroundStyle(Theme.textPrimary(colorScheme))
Spacer()
if trips.count >= 2 {
Button {
showCreatePoll = true
} label: {
Image(systemName: "plus.circle")
.foregroundStyle(Theme.warmOrange)
}
.accessibilityLabel("Create poll")
}
}
if isLoadingPolls && polls.isEmpty {
ProgressView()
.frame(maxWidth: .infinity, alignment: .center)
.padding()
} else if polls.isEmpty {
#if DEBUG
// Debug sample poll
Button {
showDebugPoll = true
} label: {
PollRowCard(poll: DebugShareExporter.buildSamplePoll())
}
.buttonStyle(.plain)
#else
emptyPollsCard
#endif
} else {
ForEach(polls) { poll in
NavigationLink(value: poll) {
PollRowCard(poll: poll)
}
.buttonStyle(.plain)
}
}
}
}
@ViewBuilder
private var emptyPollsCard: some View {
VStack(spacing: Theme.Spacing.sm) {
Image(systemName: "person.3")
.font(.title)
.foregroundStyle(Theme.textMuted(colorScheme))
.accessibilityHidden(true)
Text("No group polls yet")
.font(.subheadline)
.foregroundStyle(Theme.textSecondary(colorScheme))
if trips.count >= 2 {
Text("Create a poll to let friends vote on trip options")
.font(.caption)
.foregroundStyle(Theme.textMuted(colorScheme))
.multilineTextAlignment(.center)
} else {
Text("Save at least 2 trips to create a poll")
.font(.caption)
.foregroundStyle(Theme.textMuted(colorScheme))
.multilineTextAlignment(.center)
}
}
.frame(maxWidth: .infinity)
.padding(Theme.Spacing.lg)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
}
// MARK: - Trips Section
@ViewBuilder
private var tripsSection: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
Text("Saved Trips")
.font(.title2)
.foregroundStyle(Theme.textPrimary(colorScheme))
if trips.isEmpty {
VStack(spacing: 16) {
Image(systemName: "suitcase")
.font(.largeTitle)
.foregroundStyle(Theme.textMuted(colorScheme))
.accessibilityHidden(true)
Text("No Saved Trips")
.font(.headline)
.foregroundStyle(Theme.textPrimary(colorScheme))
Text("Browse featured trips on the Home tab or create your own to get started.")
.font(.subheadline)
.foregroundStyle(Theme.textSecondary(colorScheme))
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
.padding(Theme.Spacing.xl)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
.accessibilityIdentifier("myTrips.emptyState")
} else {
ForEach(Array(sortedTrips.enumerated()), id: \.element.id) { index, savedTrip in
if let trip = savedTrip.trip {
NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games, allowCustomItems: true)
} label: {
SavedTripListRow(trip: trip)
}
.buttonStyle(.plain)
.accessibilityIdentifier("myTrips.trip.\(index)")
.staggeredAnimation(index: index)
}
}
}
}
}
// MARK: - Actions
private func loadPolls() async {
isLoadingPolls = true
do {
polls = try await PollService.shared.fetchMyPolls()
} catch {
// Silently fail - polls just won't show
}
isLoadingPolls = false
}
}
// MARK: - Poll Row Card
private struct PollRowCard: View {
let poll: TripPoll
@Environment(\.colorScheme) private var colorScheme
var body: some View {
HStack(spacing: Theme.Spacing.md) {
// Icon
ZStack {
Circle()
.fill(Theme.warmOrange.opacity(0.15))
.frame(width: 44, height: 44)
Image(systemName: "chart.bar.doc.horizontal")
.foregroundStyle(Theme.warmOrange)
.accessibilityHidden(true)
}
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: Theme.Spacing.xs) {
Text(poll.title)
.font(.headline)
.foregroundStyle(Theme.textPrimary(colorScheme))
HStack(spacing: Theme.Spacing.sm) {
Label("\(poll.tripSnapshots.count) trips", systemImage: "map")
Text("")
Text(poll.shareCode)
.fontWeight(.semibold)
.foregroundStyle(Theme.warmOrange)
}
.font(.caption)
.foregroundStyle(Theme.textSecondary(colorScheme))
}
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundStyle(Theme.textMuted(colorScheme))
.accessibilityHidden(true)
}
.padding(Theme.Spacing.md)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
.shadow(color: Theme.cardShadow(colorScheme), radius: 6, y: 3)
}
}
struct SavedTripListRow: View {
let trip: Trip
@Environment(\.colorScheme) private var colorScheme
var body: some View {
HStack(spacing: Theme.Spacing.md) {
// Route preview
VStack(spacing: 4) {
ForEach(0..<min(3, trip.stops.count), id: \.self) { i in
Circle()
.fill(Theme.warmOrange.opacity(Double(3 - i) / 3))
.frame(width: 8, height: 8)
if i < min(2, trip.stops.count - 1) {
Rectangle()
.fill(Theme.routeGold.opacity(0.5))
.frame(width: 2, height: 8)
}
}
}
.frame(width: 20)
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: Theme.Spacing.xs) {
Text(trip.displayName)
.font(.headline)
.foregroundStyle(Theme.textPrimary(colorScheme))
Text(trip.formattedDateRange)
.font(.subheadline)
.foregroundStyle(Theme.textSecondary(colorScheme))
// Route preview strip
if !trip.stops.isEmpty {
RoutePreviewStrip(cities: trip.stops.map { $0.city })
}
// Stats
HStack(spacing: Theme.Spacing.md) {
StatPill(icon: "mappin.circle", value: "\(trip.stops.count) cities")
StatPill(icon: "sportscourt", value: "\(trip.totalGames) games")
}
}
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundStyle(Theme.textMuted(colorScheme))
.accessibilityHidden(true)
}
.padding(Theme.Spacing.lg)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
.shadow(color: Theme.cardShadow(colorScheme), radius: 8, y: 4)
.accessibilityElement(children: .combine)
}
}
// MARK: - Pro Locked View
struct ProLockedView: View {
let feature: ProFeature
let onUnlock: () -> Void
@Environment(\.colorScheme) private var colorScheme
var body: some View {
VStack(spacing: Theme.Spacing.xl) {
Spacer()
ZStack {
Circle()
.fill(Theme.warmOrange.opacity(0.15))
.frame(width: 100, height: 100)
Image(systemName: "lock.fill")
.font(.largeTitle)
.foregroundStyle(Theme.warmOrange)
.accessibilityHidden(true)
}
.accessibilityLabel("Pro feature locked")
VStack(spacing: Theme.Spacing.sm) {
Text(feature.displayName)
.font(.title2.bold())
.foregroundStyle(Theme.textPrimary(colorScheme))
Text(feature.description)
.font(.body)
.foregroundStyle(Theme.textSecondary(colorScheme))
.multilineTextAlignment(.center)
.padding(.horizontal, Theme.Spacing.xl)
}
Button {
onUnlock()
} label: {
HStack {
Image(systemName: "star.fill")
Text("Upgrade to Pro")
}
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
.padding(Theme.Spacing.md)
.background(Theme.warmOrange)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
.accessibilityElement(children: .combine)
}
.padding(.horizontal, Theme.Spacing.xl)
Spacer()
Spacer()
}
.themedBackground()
}
}
#Preview {
HomeView()
.modelContainer(for: SavedTrip.self, inMemory: true)
}