Skip to content

Commit dac9019

Browse files
Working on updating detail view w/ playadb
1 parent cf3c5f4 commit dac9019

24 files changed

Lines changed: 1003 additions & 236 deletions

CLAUDE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ xcodebuild test -workspace iBurn.xcworkspace -scheme iBurnTests -destination 'pl
8989

9090
# Run PlayaKit tests
9191
xcodebuild test -workspace iBurn.xcworkspace -scheme PlayaKitTests -destination 'platform=iOS Simulator,name=iPhone 16 Pro,arch=arm64' -quiet
92+
93+
# Run SPM tests (quietly by default)
94+
swift test --quiet
9295
```
9396

9497
**Utility Commands**:
@@ -121,9 +124,6 @@ xcrun simctl shutdown "iPhone 16 Pro"
121124
xcrun simctl erase "iPhone 16 Pro"
122125
```
123126

124-
125-
126-
127127
### Fastlane Commands
128128
- `fastlane ios beta` - Build and upload to TestFlight
129129
- `fastlane ios refresh_dsyms` - Download and upload crash symbols
@@ -134,7 +134,7 @@ When adding new functionality, make sure to plan for testability. When your feat
134134

135135
- **Command Line**: Use xcodebuild test commands shown above for automated testing
136136
- **Xcode GUI**: Run tests through Xcode Test Navigator or `Cmd+U`
137-
- **Test targets**: `iBurnTests`, `PlayaKitTests`
137+
- **Test targets**: `iBurnTests`, `PlayaKitTests`, and local Swift Package targets for `PlayaDB` and `PlayaAPI`
138138

139139
## Architecture Overview
140140

Docs/2026-01-25-playadb-migration-next-steps.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,52 @@ xcodebuild test -workspace iBurn.xcworkspace -scheme iBurnTests -destination 'pl
178178
- Users can browse Events using PlayaDB data (including correct cross-midnight occurrences) behind a feature flag.
179179
- Tapping an event can still route to the existing legacy detail flow until event detail is migrated.
180180
- The app continues to build and `xcodebuild test -scheme iBurnTests` continues to pass on simulator.
181+
182+
---
183+
184+
## Next Plan: Remove LegacyDataStore From SwiftUI List Navigation (Art/Camps)
185+
186+
### Goal
187+
- SwiftUI Art/Camp lists should navigate (detail + map) without using YapDatabase-backed `LegacyDataStore` at all.
188+
- Allowed dependencies: PlayaDB (GRDB) + `BRCMediaDownloader` (filesystem/bundled media). Audio continues to use `BRCAudioPlayer` (now supports `BRCAudioTourTrack`).
189+
190+
### Plan (Incremental)
191+
1) **Detail flow (PlayaDB-only)**
192+
- Add new SwiftUI detail views for `PlayaDB.ArtObject` and `PlayaDB.CampObject`.
193+
- Back them with a small view model that:
194+
- reads `ObjectMetadata` via `PlayaDB.metadata(for:)`
195+
- toggles favorites via `PlayaDB.toggleFavorite(_:)`
196+
- loads thumbnail/audio URLs via `MediaAssetProviding` (`BRCMediaDownloader.localMediaURL`)
197+
- optionally supports notes + lastViewed (requires PlayaDB API additions below)
198+
199+
2) **Metadata write support**
200+
- Extend `Packages/PlayaDB` public API with explicit metadata update methods (notes + lastViewed) so the app can persist those without reaching into GRDB internals.
201+
- Add unit tests in `PlayaKitTests` or `iBurnTests` (whichever already hosts PlayaDB tests) that verify:
202+
- notes round-trip
203+
- lastViewed updates
204+
- updatedAt changes appropriately
205+
206+
3) **Map flow (PlayaDB-only)**
207+
- Implement a new annotation type (e.g. `PlayaObjectAnnotation`) that conforms to `MLNAnnotation` + `ImageAnnotation` and is built from PlayaDB objects (uid, type, coordinate, title/subtitle).
208+
- Enforce embargo via `BRCEmbargo.allowEmbargoedData()` (if embargoed, don’t show art/camp pins).
209+
210+
4) **Map callout -> Detail**
211+
- Update `MapViewAdapter` to:
212+
- render `PlayaObjectAnnotation` using `LabelAnnotationView` (same look as legacy)
213+
- route info-tap to the new detail flow via an injected closure/delegate (so we don’t call `DetailViewControllerFactory`).
214+
- Defer share QR action for Playa objects until we have a PlayaDB-compatible share payload.
215+
216+
5) **Replace LegacyDataStore usage**
217+
- Update:
218+
- `iBurn/ListView/ArtListHostingController.swift`
219+
- `iBurn/ListView/CampListHostingController.swift`
220+
- `onSelect`: push the new PlayaDB detail hosting controller.
221+
- `onShowMap`: build `PlayaObjectAnnotation`s from the current filtered items and push `MapListViewController(dataSource: StaticAnnotationDataSource(annotations: ...))`.
222+
223+
6) **Verification**
224+
- Verify on iOS 26.2 sim:
225+
- list -> detail works
226+
- list -> map works
227+
- map callout info -> detail works
228+
- favorites + notes persist via PlayaDB
229+
- embargo hides art/camp pins when locked

Packages/PlayaAPI/Sources/PlayaAPI/Models/Shared/Identifiers.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,4 @@ public typealias ArtID = ID<Art>
4242
public typealias CampID = ID<Camp>
4343

4444
/// Strongly-typed identifier for Event objects
45-
public typealias EventID = ID<Event>
45+
public typealias EventID = ID<Event>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import Foundation
2+
import PlayaAPI
3+
4+
/// A single identifier type that can represent any PlayaDB object identifier while
5+
/// preserving the underlying strongly-typed PlayaAPI IDs.
6+
///
7+
/// Useful for heterogeneous collections (e.g. map annotations, deep links, routing).
8+
public enum AnyDataObjectID: Hashable, Sendable, Codable {
9+
case art(ArtID)
10+
case camp(CampID)
11+
case event(EventID)
12+
13+
public var objectType: DataObjectType {
14+
switch self {
15+
case .art: return .art
16+
case .camp: return .camp
17+
case .event: return .event
18+
}
19+
}
20+
21+
/// The raw uid string used by PlayaDB tables.
22+
public var uid: String {
23+
switch self {
24+
case .art(let id): return id.value
25+
case .camp(let id): return id.value
26+
case .event(let id): return id.value
27+
}
28+
}
29+
30+
public init(objectType: DataObjectType, uid: String) {
31+
switch objectType {
32+
case .art:
33+
self = .art(ArtID(uid))
34+
case .camp:
35+
self = .camp(CampID(uid))
36+
case .event:
37+
self = .event(EventID(uid))
38+
}
39+
}
40+
}
41+
42+
public extension DataObject {
43+
/// Converts a concrete PlayaDB model into a single sum-type identifier.
44+
var anyID: AnyDataObjectID {
45+
AnyDataObjectID(objectType: objectType, uid: uid)
46+
}
47+
}
48+

Packages/PlayaDB/Sources/PlayaDB/PlayaDB.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ public protocol PlayaDB {
114114

115115
/// Check if an object is favorited
116116
func isFavorite(_ object: any DataObject) async throws -> Bool
117+
118+
/// Update user notes for an object (nil/empty clears notes).
119+
func setUserNotes(_ notes: String?, for object: any DataObject) async throws
120+
121+
/// Mark an object as viewed at the provided date (used for recents, etc.).
122+
func setLastViewed(_ date: Date, for object: any DataObject) async throws
117123

118124
// MARK: - Data Import
119125

Packages/PlayaDB/Sources/PlayaDB/PlayaDBImpl.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,41 @@ internal class PlayaDBImpl: PlayaDB {
984984
return metadata?.isFavorite ?? false
985985
}
986986
}
987+
988+
func setUserNotes(_ notes: String?, for object: any DataObject) async throws {
989+
try await ensureMetadata(for: object.objectType, ids: [object.uid])
990+
991+
try await dbQueue.write { db in
992+
guard var metadata = try ObjectMetadata
993+
.filter(ObjectMetadata.Columns.objectType == object.objectType.rawValue)
994+
.filter(ObjectMetadata.Columns.objectId == object.uid)
995+
.fetchOne(db) else {
996+
throw PlayaDBError.metadataNotFound
997+
}
998+
999+
let trimmed = notes?.trimmingCharacters(in: .whitespacesAndNewlines)
1000+
metadata.userNotes = (trimmed?.isEmpty == true) ? nil : trimmed
1001+
metadata.updatedAt = Date()
1002+
try metadata.update(db)
1003+
}
1004+
}
1005+
1006+
func setLastViewed(_ date: Date, for object: any DataObject) async throws {
1007+
try await ensureMetadata(for: object.objectType, ids: [object.uid])
1008+
1009+
try await dbQueue.write { db in
1010+
guard var metadata = try ObjectMetadata
1011+
.filter(ObjectMetadata.Columns.objectType == object.objectType.rawValue)
1012+
.filter(ObjectMetadata.Columns.objectId == object.uid)
1013+
.fetchOne(db) else {
1014+
throw PlayaDBError.metadataNotFound
1015+
}
1016+
1017+
metadata.lastViewed = date
1018+
metadata.updatedAt = Date()
1019+
try metadata.update(db)
1020+
}
1021+
}
9871022

9881023
// MARK: - Data Import
9891024

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Foundation
2+
import PlayaAPI
3+
4+
// Convenience typed identifiers for PlayaDB models.
5+
//
6+
// PlayaDB stores `uid` as a String for database/GRDB ergonomics, but the app often
7+
// wants strongly-typed IDs (from PlayaAPI) when routing/navigation crosses layers.
8+
9+
extension ArtObject: Identifiable {
10+
public var id: ArtID { ArtID(uid) }
11+
}
12+
13+
extension CampObject: Identifiable {
14+
public var id: CampID { CampID(uid) }
15+
}
16+
17+
extension EventObject: Identifiable {
18+
public var id: EventID { EventID(uid) }
19+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import XCTest
2+
@testable import PlayaDB
3+
import PlayaAPI
4+
5+
final class AnyDataObjectIDTests: XCTestCase {
6+
func testInitFromObjectTypeAndUID() {
7+
XCTAssertEqual(AnyDataObjectID(objectType: .art, uid: "a").uid, "a")
8+
XCTAssertEqual(AnyDataObjectID(objectType: .camp, uid: "c").uid, "c")
9+
XCTAssertEqual(AnyDataObjectID(objectType: .event, uid: "e").uid, "e")
10+
}
11+
12+
func testRoundTripCodable() throws {
13+
let values: [AnyDataObjectID] = [
14+
.art(ArtID("a1")),
15+
.camp(CampID("c1")),
16+
.event(EventID("e1")),
17+
]
18+
19+
let data = try JSONEncoder().encode(values)
20+
let decoded = try JSONDecoder().decode([AnyDataObjectID].self, from: data)
21+
XCTAssertEqual(decoded, values)
22+
}
23+
24+
func testUIDAndObjectType() {
25+
let id = AnyDataObjectID.art("abc")
26+
XCTAssertEqual(id.uid, "abc")
27+
XCTAssertEqual(id.objectType, .art)
28+
}
29+
}
30+
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import XCTest
2+
@testable import PlayaDB
3+
import PlayaAPITestHelpers
4+
5+
final class MetadataUpdateTests: XCTestCase {
6+
private var playaDB: PlayaDB!
7+
8+
override func setUp() async throws {
9+
try await super.setUp()
10+
playaDB = try PlayaDBImpl(dbPath: ":memory:")
11+
12+
try await playaDB.importFromData(
13+
artData: MockAPIData.artJSON,
14+
campData: MockAPIData.campJSON,
15+
eventData: MockAPIData.eventJSON
16+
)
17+
}
18+
19+
override func tearDown() async throws {
20+
playaDB = nil
21+
try await super.tearDown()
22+
}
23+
24+
func testSetUserNotesRoundTrip() async throws {
25+
let arts = try await playaDB.fetchArt()
26+
let art = try XCTUnwrap(arts.first)
27+
28+
try await playaDB.setUserNotes("hello", for: art)
29+
let metadata1 = try await playaDB.metadata(for: art)
30+
XCTAssertEqual(metadata1.userNotes, "hello")
31+
32+
try await playaDB.setUserNotes("", for: art)
33+
let metadata2 = try await playaDB.metadata(for: art)
34+
XCTAssertNil(metadata2.userNotes)
35+
}
36+
37+
func testSetLastViewedUpdatesMetadata() async throws {
38+
let arts = try await playaDB.fetchArt()
39+
let art = try XCTUnwrap(arts.first)
40+
41+
let date = Date(timeIntervalSince1970: 123)
42+
try await playaDB.setLastViewed(date, for: art)
43+
let metadata = try await playaDB.metadata(for: art)
44+
XCTAssertEqual(metadata.lastViewed, date)
45+
}
46+
}

Packages/PlayaDB/Tests/PlayaDBTests/PlayaDBImportTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,8 @@ final class PlayaDBImportTests: XCTestCase {
250250
let campData = MockAPIData.campJSON
251251
let eventData = MockAPIData.eventJSON
252252
let parser = APIParserFactory.create()
253-
let expectedEventObjects = try parser.parseEvents(from: eventData)
254-
// let totalExpectedOccurrences = expectedEventObjects.reduce(0) { $0 + $1.occurrenceSet.count }
253+
_ = try parser.parseEvents(from: eventData)
254+
// Note: we currently don't assert occurrence counts here; this parse call just sanity-checks the fixture.
255255

256256
// When: Import data into PlayaDB
257257
try await playaDB.importFromData(artData: artData, campData: campData, eventData: eventData)
@@ -388,4 +388,4 @@ final class PlayaDBImportTests: XCTestCase {
388388
XCTAssertGreaterThan(eventObject.year, 0, "Event object should have valid year")
389389
}
390390
}
391-
}
391+
}

0 commit comments

Comments
 (0)