From 70d8299df8858c505c4231adac2304a10e6faf13 Mon Sep 17 00:00:00 2001 From: Trey T Date: Mon, 11 May 2026 10:36:16 -0500 Subject: [PATCH] =?UTF-8?q?Books=20=E2=80=94=20robust=20bundle=20lookup=20?= =?UTF-8?q?so=20ad-hoc=20device=20installs=20always=20seed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bundledBookJSONURLs() was relying solely on Bundle.urls(forResourcesWithExtension:subdirectory:), the directory- enumeration API. That API is observed to return empty in some on-device configurations even when the resource is present, which would cause seedBooks() to silently no-op and leave BookLibraryView showing "No Books" despite the JSON being bundled correctly. Switched to the same pattern textbook seeding uses: explicit per-slug Bundle.url(forResource:withExtension:) with a bundleURL.appending- PathComponent fallback. Directory enumeration is kept as a secondary so future books bundled without code changes still get picked up. Also added diagnostic prints to refreshBooksDataIfNeeded and the URL discovery step so device logs reveal what happened if seeding still falls through. Bumped bookDataVersion to 3 so existing installs re-trigger refreshBooksDataIfNeeded → seedBooks on next launch. Co-Authored-By: Claude Opus 4.7 (1M context) --- Conjuga/Conjuga/Services/DataLoader.swift | 48 +++++++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/Conjuga/Conjuga/Services/DataLoader.swift b/Conjuga/Conjuga/Services/DataLoader.swift index 10bbbd4..f2db7c2 100644 --- a/Conjuga/Conjuga/Services/DataLoader.swift +++ b/Conjuga/Conjuga/Services/DataLoader.swift @@ -9,7 +9,7 @@ actor DataLoader { static let textbookDataVersion = 14 static let textbookDataKey = "textbookDataVersion" - static let bookDataVersion = 2 // bump: vocab
  • bullets now extracted + static let bookDataVersion = 3 // bump: robust bundle lookup with explicit-slug fallback static let bookDataKey = "bookDataVersion" /// Quick check: does the DB need seeding or course data refresh? @@ -163,7 +163,10 @@ actor DataLoader { let shared = UserDefaults.standard let context = ModelContext(container) let existingCount = (try? context.fetchCount(FetchDescriptor())) ?? 0 - let versionCurrent = shared.integer(forKey: bookDataKey) >= bookDataVersion + let storedVersion = shared.integer(forKey: bookDataKey) + let versionCurrent = storedVersion >= bookDataVersion + + print("[DataLoader] refreshBooksDataIfNeeded: existing=\(existingCount) stored=\(storedVersion) target=\(bookDataVersion) versionCurrent=\(versionCurrent)") if versionCurrent && existingCount > 0 { return } @@ -182,7 +185,9 @@ actor DataLoader { if seedBooks(context: context) { shared.set(bookDataVersion, forKey: bookDataKey) - print("Book data re-seeded to version \(bookDataVersion)") + print("[DataLoader] Book data re-seeded to version \(bookDataVersion)") + } else { + print("[DataLoader] Book reseed produced no rows — leaving version key untouched") } } @@ -637,18 +642,45 @@ actor DataLoader { return true } - /// Find every `book_*.json` resource in the app bundle. + /// Slugs of books bundled with the app. Kept explicit so device installs + /// don't depend on `Bundle.urls(forResourcesWithExtension:subdirectory:)` + /// successfully enumerating the bundle — that API has been observed to + /// return empty for some iOS configurations even when the resource is + /// present, matching the same `bundleURL.appendingPathComponent` fallback + /// used by the textbook seed. + private static let bundledBookSlugs: [String] = [ + "olly-vol2", + ] + + /// Resolve URLs for every bundled book. Uses the explicit-slug fast path + /// first (mirrors `seedTextbookData`'s lookup pattern), then falls back to + /// directory enumeration so newly-bundled books are picked up without a + /// code change. private static func bundledBookJSONURLs() -> [URL] { var seen = Set() var out: [URL] = [] let bundle = Bundle.main - for ext in ["json"] { - if let urls = bundle.urls(forResourcesWithExtension: ext, subdirectory: nil) { - for url in urls where url.lastPathComponent.hasPrefix("book_") { - if seen.insert(url.lastPathComponent).inserted { out.append(url) } + + for slug in bundledBookSlugs { + let filename = "book_\(slug).json" + let url = bundle.url(forResource: "book_\(slug)", withExtension: "json") + ?? bundle.bundleURL.appendingPathComponent(filename) + if FileManager.default.fileExists(atPath: url.path), + seen.insert(filename).inserted { + out.append(url) + } + } + + if let urls = bundle.urls(forResourcesWithExtension: "json", subdirectory: nil) { + for url in urls where url.lastPathComponent.hasPrefix("book_") { + if seen.insert(url.lastPathComponent).inserted { + out.append(url) } } } + + let names = out.map(\.lastPathComponent).joined(separator: ", ") + print("[DataLoader] bundledBookJSONURLs found \(out.count) files: [\(names)]") return out.sorted { $0.lastPathComponent < $1.lastPathComponent } }