-
Notifications
You must be signed in to change notification settings - Fork 0
Capstone Checkpoint 4 #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0d6b1b2
396dc9b
a912592
b2ebd69
5177e4f
329b2f9
e95a164
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| import Foundation | ||
| import RealityKit | ||
| import ArtifactScenes | ||
|
|
||
| enum SceneLoadingState { | ||
| case notLoaded | ||
| case loading | ||
| case loaded | ||
| case failed(Error) | ||
| } | ||
|
|
||
| class ArtifactScenesViewModel: ObservableObject { | ||
| @Published var selectedSceneName: String | ||
| @Published private(set) var sceneLoadingState: SceneLoadingState = .notLoaded | ||
|
|
||
| private var sceneCache: [String: Entity] = [:] | ||
| private var preloadTasks: Set<Task<Void, Never>> = [] | ||
| private let maxCacheSize = 4 | ||
|
|
||
| private let journeyProgressManager = JourneyProgressManager() | ||
|
|
||
| private var currentIndex: Int = 0 | ||
| private var artifacts: [Artifact] = [] | ||
| private var journeyPrefix: String = "" | ||
|
|
||
| init(initialSceneName: String, artifacts: [Artifact] = [], journeyPrefix: String = "") { | ||
| self.selectedSceneName = initialSceneName | ||
| self.artifacts = artifacts | ||
| self.journeyPrefix = journeyPrefix | ||
|
|
||
| if let initialIndex = artifacts.firstIndex(where: { $0.sceneName == initialSceneName }) { | ||
| self.currentIndex = initialIndex | ||
| } | ||
| } | ||
|
|
||
| func selectScene(named sceneName: String) async { | ||
| // Run UI updates on main thread | ||
| await MainActor.run { | ||
| if let newIndex = artifacts.firstIndex(where: { $0.sceneName == sceneName }) { | ||
| currentIndex = newIndex | ||
| selectedSceneName = sceneName | ||
| } | ||
| } | ||
|
|
||
| cancelPreloadTasks() | ||
| await loadSelectedAndPreloadNext() | ||
|
|
||
| // if the scene loaded successfully, | ||
| // mark it as viewed in UserDefaults | ||
| if case .loaded = sceneLoadingState { | ||
| journeyProgressManager.markArtifactViewed(journeyPrefix: journeyPrefix, artifactName: sceneName) | ||
| } | ||
| } | ||
|
|
||
| private func loadSelectedAndPreloadNext() async { | ||
| // Load current scene if needed | ||
| await loadSceneIfNeeded(named: selectedSceneName, priority: .high) | ||
|
|
||
| // Preload next scenes | ||
| await preloadUpcomingScenes() | ||
| cleanupCache() | ||
| } | ||
|
|
||
| private func loadSceneIfNeeded(named sceneName: String, priority: TaskPriority = .medium) async { | ||
| guard sceneCache[sceneName] == nil else { return } | ||
|
|
||
| // Update loading state on main thread | ||
| await MainActor.run { | ||
| sceneLoadingState = .loading | ||
| } | ||
|
|
||
| do { | ||
| let scene = try await Entity(named: "\(journeyPrefix)/\(journeyPrefix)_\(sceneName)", | ||
| in: artifactScenesBundle) | ||
| // Cache and update state on main thread | ||
| await MainActor.run { | ||
| sceneCache[sceneName] = scene | ||
| sceneLoadingState = .loaded | ||
| } | ||
| } catch { | ||
| print("Error loading scene \(sceneName): \(error)") | ||
| await MainActor.run { | ||
| sceneLoadingState = .failed(error) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private func cancelPreloadTasks() { | ||
| preloadTasks.forEach { $0.cancel() } | ||
| preloadTasks.removeAll() | ||
| } | ||
|
|
||
| private func preloadUpcomingScenes() async { | ||
| // Determine next scenes to preload | ||
| let upcomingIndices = getUpcomingIndices() | ||
|
|
||
| // Create preload tasks | ||
| for index in upcomingIndices { | ||
| let sceneName = artifacts[index].sceneName | ||
| let task = Task(priority: .low) { | ||
| await loadSceneIfNeeded(named: sceneName) | ||
| } | ||
| preloadTasks.insert(task) | ||
| } | ||
| } | ||
|
|
||
| private func getUpcomingIndices() -> [Int] { | ||
| let nextIndices = (currentIndex + 1)...(currentIndex + 2) | ||
| return nextIndices.filter { $0 < artifacts.count } | ||
| } | ||
|
|
||
| private func cleanupCache() { | ||
| // Keep only current and adjacent models in cache | ||
| let keepIndices = Set((max(0, currentIndex - 1)...min(artifacts.count - 1, currentIndex + 2))) | ||
| let keepSceneNames = Set(keepIndices.map { artifacts[$0].sceneName }) | ||
|
|
||
| // Remove scenes that are no longer needed | ||
| sceneCache = sceneCache.filter { keepSceneNames.contains($0.key) } | ||
| } | ||
|
|
||
| func getCachedScene(named sceneName: String) -> Entity? { | ||
| return sceneCache[sceneName] | ||
| } | ||
|
|
||
| deinit { | ||
| cancelPreloadTasks() | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,20 @@ | ||
| struct Journey { | ||
| struct Journey: Codable, Identifiable { | ||
| let imageUrl: String | ||
| let title: String | ||
| let description: String | ||
| let artifactPrefix: String | ||
| let artifacts: [Artifact] | ||
|
|
||
| var id: String { title } | ||
|
|
||
| static let sampleJourneys = [ | ||
| Journey(imageUrl: "https://artifact-ios.s3.us-east-2.amazonaws.com/7.jpg", title: "7 wonders", description: "Embark on a journey to explore the legendary New 7 Wonders of the World, marvels that span continents and time. From the ancient walls that stretch across China to the towering statue overlooking Rio, each wonder tells a story of human achievement and cultural legacy. Encounter the architectural splendor of Petra, the timeless beauty of the Taj Mahal, and the majestic terraces of Machu Picchu. Stand before the enduring grandeur of Chichen Itza and the epic arches of the Roman Colosseum. This journey isn’t just about visiting places—it’s a path through history, mystery, and the extraordinary tales behind the world’s most iconic structures. Let each wonder inspire awe, ignite curiosity, and deepen your connection to the world’s shared heritage.", artifactPrefix: "7", artifacts: [Artifact(sceneName: "Colosseum", info: "This ancient amphitheater in Rome hosted gladiator battles and public spectacles, embodying the grandeur of Roman engineering and the era’s complex social life."), Artifact(sceneName: "Great_Wall", info: "A massive architectural feat, the Great Wall stretches thousands of miles across northern China, originally built to protect against invasions and now a symbol of resilience and history."), Artifact(sceneName: "Petra", info: "Known as the “Rose City” for its pink sandstone cliffs, Petra is an ancient city carved into rock, showcasing the Nabatean civilization's architectural and engineering prowess."),Artifact(sceneName: "Christ_the_Redeemer", info: "Towering above Rio de Janeiro, this colossal statue of Jesus Christ stands with open arms, symbolizing peace, protection, and a warm welcome to visitors from all over the world."), Artifact(sceneName: "Machu_Picchu", info: "Nestled high in the Andes, this Inca citadel is a marvel of ancient engineering, blending seamlessly with the rugged mountain landscape and offering insights into a lost civilization."), Artifact(sceneName: "Taj_Mahal", info: "Built as a monument of love, the Taj Mahal is a stunning marble mausoleum with intricate inlay work, gardens, and symmetry, reflecting the beauty and devotion of the Mughal era."), Artifact(sceneName: "Chichen_Itza", info: "Once a thriving Maya city, Chichen Itza is home to the iconic El Castillo pyramid, an architectural masterpiece that reveals the Maya’s advanced understanding of astronomy.")]), | ||
| Journey(imageUrl: "https://artifact-ios.s3.us-east-2.amazonaws.com/solar-system.jpg", title: "Solar System", description: "Embark on a journey across the vast expanses of our solar system, where each planet holds unique mysteries and characteristics. From the fiery surface of Mercury to the distant, icy reaches of Neptune, explore the wonders orbiting our Sun. Glide past the swirling clouds of Jupiter, marvel at the rings of Saturn, and witness the vibrant landscapes of Mars. This journey invites you to experience the solar system’s scale, diversity, and the celestial beauty that lies beyond Earth, connecting us to the greater cosmos with every orbit.", artifactPrefix: "Solar", artifacts: [Artifact(sceneName: "Mercury", info: "The smallest planet and closest to the Sun, Mercury has extreme temperature shifts and a cratered surface, resembling Earth’s moon in appearance."), Artifact(sceneName: "Venus", info: "Often called Earth’s “sister planet” due to its similar size, Venus has a thick, toxic atmosphere and surface temperatures hot enough to melt lead."), Artifact(sceneName: "Earth", info: "The only planet known to support life, Earth has diverse ecosystems, abundant water, and a dynamic climate, making it uniquely habitable."), Artifact(sceneName: "Mars", info: "Known as the “Red Planet” for its rusty surface, Mars has vast deserts, canyons, and polar ice caps, and continues to intrigue scientists with the possibility of past water."), Artifact(sceneName: "Jupiter", info: "The largest planet, Jupiter is a gas giant with powerful storms, including the iconic Great Red Spot, and dozens of moons, making it a mini solar system."), Artifact(sceneName: "Saturn", info: "Famous for its stunning ring system, Saturn is a gas giant with a unique atmosphere and many moons, including Titan, which has its own weather patterns."), Artifact(sceneName: "Uranus", info: "Known for its unusual tilt, Uranus orbits the Sun on its side, has faint rings, and displays a pale blue color due to its icy atmosphere."), Artifact(sceneName: "Neptune", info: "The windiest planet in the solar system, Neptune is a deep blue gas giant with faint rings and a distant, cold orbit that marks the edge of the known planets.")])] | ||
| } | ||
|
|
||
| struct Artifact: Identifiable { | ||
| struct Artifact: Identifiable, Codable { | ||
| let sceneName: String | ||
| let info: String | ||
| var id: String { | ||
| sceneName | ||
| } | ||
|
|
||
| var id: String { sceneName } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import Foundation | ||
|
|
||
| class JourneyProgressManager { | ||
| private let defaults = UserDefaults.standard | ||
| private let viewedArtifactsKey = "viewedArtifacts" | ||
|
|
||
| func getViewedArtifacts(for journeyPrefix: String) -> Set<String> { | ||
| let key = "\(viewedArtifactsKey)_\(journeyPrefix)" | ||
| return Set(defaults.stringArray(forKey: key) ?? []) | ||
| } | ||
|
|
||
| func markArtifactViewed(journeyPrefix: String, artifactName: String) { | ||
| let key = "\(viewedArtifactsKey)_\(journeyPrefix)" | ||
| var viewed = getViewedArtifacts(for: journeyPrefix) | ||
| viewed.insert(artifactName) | ||
| defaults.set(Array(viewed), forKey: key) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import Foundation | ||
|
|
||
| class JourneyService { | ||
| private let apiURL = "https://gn4xt2b916.execute-api.us-east-2.amazonaws.com/prod/journeys" | ||
| private let apiKey = "tP731AxMWA61ISM5XIaUf3XSdLQf8n3EnC8Jc660" // throttled, so ok to be public | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. API keys should not be in Git Hub repositories. |
||
|
|
||
| func fetchJourneys() async throws -> [Journey] { | ||
| var request = URLRequest(url: URL(string: apiURL)!) | ||
| request.setValue(apiKey, forHTTPHeaderField: "x-api-key") | ||
| let (data, _) = try await URLSession.shared.data(for: request) | ||
| return try JSONDecoder().decode([Journey].self, from: data) | ||
| } | ||
| } | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import Foundation | ||
|
|
||
| @MainActor | ||
| class JourneysViewModel: ObservableObject { | ||
| @Published var journeys: [Journey] = [] | ||
| @Published var isLoading = false | ||
| @Published var error: Error? | ||
|
|
||
| private let service = JourneyService() | ||
|
|
||
| func loadJourneys() { | ||
| isLoading = true | ||
|
|
||
| Task { | ||
| do { | ||
| journeys = try await service.fetchJourneys() | ||
| } catch { | ||
| self.error = error | ||
| } | ||
| isLoading = false | ||
| } | ||
| } | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,13 @@ | ||
| import SwiftUI | ||
|
|
||
| struct ContentView: View { | ||
| @StateObject private var journeysViewModel = JourneysViewModel() | ||
|
|
||
| var body: some View { | ||
| NavigationView { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NavigationView is deprecated. You should consider using NavigationStack instead. |
||
| ScrollView { | ||
| VStack(alignment: .leading, spacing: 16) { | ||
| ForEach(Journey.sampleJourneys, id: \.self.title) { journey in | ||
| ForEach(journeysViewModel.journeys, id: \.self.title) { journey in | ||
| NavigationLink(destination: JourneyDetailView(journey)) { | ||
| JourneyCardView(journey) | ||
| } | ||
|
|
@@ -15,6 +17,9 @@ struct ContentView: View { | |
| .padding() | ||
| } | ||
| } | ||
| .onAppear { | ||
| journeysViewModel.loadJourneys() | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using the Observation framework instead of ObservableObjects. Your app target is 18.1, so there is no reason to use the old API.