Skip to content

Commit 64fd0f1

Browse files
committed
Phase 2: SwiftData versioning and persistence facade
1 parent 1cfdc65 commit 64fd0f1

19 files changed

+247
-181
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,4 @@ fastlane/test_output
9393
iOSInjectionProject/
9494
AGENTS/
9595
AGENTS.md
96+
.plan/*.log

.plan/IMPLEMENTATION_PLAN.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,18 @@
3535
- [x] Ensure all SwiftData `ModelContext` operations run on its owning executor (main actor for `@Environment` context)
3636
- [x] Confirm build log has zero strict-concurrency warnings for these files
3737

38+
### Phase 1.1 — Warning cleanup (compiler + SwiftLint baseline)
39+
- [x] Eliminate unused-result compiler warnings (use `@discardableResult` for fluent APIs)
40+
- [x] Fix `CalmSound.id` decoding warning (make `id` decodable/mutable)
41+
- [x] Fix SwiftLint accessibility warnings (labels/hidden images; tap gestures treated as buttons)
42+
- [x] Fix SwiftLint `line_length` warnings (wrap long strings and interpolations)
43+
- [x] Confirm build log has zero compiler warnings and zero SwiftLint violations (baseline)
44+
3845
### Phase 2 — Data model and persistence boundary hardening
39-
- [ ] Decide whether `saved`/`deleted` in [TrackItem.swift](file:///Users/gc/Developer/Aemi%20Studio.nosync/Pasitea/Pasitea/Model/TrackItem.swift#L12-L60) should be persisted or `@Transient`
40-
- [ ] Add SwiftData schema versioning and migration strategy (if any user data exists)
41-
- [ ] Introduce a persistence facade (main-actor or model-actor) and route saves through it
42-
- [ ] Stop calling `ModelContext.save()` directly from views (only via the facade)
46+
- [x] Decide whether `saved`/`deleted` in [TrackItem.swift](file:///Users/gc/Developer/Aemi%20Studio.nosync/Pasitea/Pasitea/Model/TrackItem.swift#L12-L60) should be persisted or `@Transient` (removed; no longer persisted)
47+
- [x] Add SwiftData schema versioning and migration strategy (if any user data exists)
48+
- [x] Introduce a persistence facade (main-actor or model-actor) and route saves through it
49+
- [x] Stop calling `ModelContext.save()` directly from views (only via the facade)
4350

4451
### Phase 3 — Safety cleanup (crash-proofing)
4552
- [ ] Replace force unwrap of `TrackType(rawValue:)` in [TrackItem.swift](file:///Users/gc/Developer/Aemi%20Studio.nosync/Pasitea/Pasitea/Model/TrackItem.swift#L62-L82)

Pasitea/Model/CalmSound.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import Foundation
99

1010
struct CalmSound: Codable, Hashable, Identifiable {
11-
let id: UUID = UUID()
11+
var id: UUID = UUID()
1212
var title: String
1313
var filename: String
1414
}

Pasitea/Model/TrackItem.swift

Lines changed: 91 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -9,56 +9,54 @@ import Foundation
99
import SwiftData
1010
import SwiftUI
1111

12-
@Model
13-
public final class TrackItem: Identifiable {
14-
@Attribute(.unique) public var id: UUID
15-
public var type: TrackType.RawValue = TrackType.none.rawValue
16-
public var desc: String = ""
17-
public var tags: [String] = []
18-
public var startDate: Date = Date.now
19-
public var endDate: Date = Date.now
20-
public var previousId: UUID?
21-
private var saved: Bool = false
22-
private var deleted: Bool = false
23-
24-
enum TrackItemCodingKeys: CodingKey {
25-
case id
26-
case type
27-
case desc
28-
case tags
29-
case startDate
30-
case endDate
31-
case previousId
32-
}
12+
public enum PasiteaSchemaV1: VersionedSchema {
13+
public static var versionIdentifier: Schema.Version { Schema.Version(1, 0, 0) }
3314

34-
init(
35-
type: TrackType = TrackType.none,
36-
desc: String = "",
37-
tags: [String] = [],
38-
startDate: Date = Date.now,
39-
endDate: Date = Date.now,
40-
previousId: UUID? = nil
41-
) {
42-
self.id = UUID()
43-
self.type = type.rawValue
44-
self.desc = desc
45-
self.tags = tags
46-
self.startDate = startDate
47-
self.endDate = endDate
48-
self.previousId = previousId ?? nil
15+
public static var models: [any PersistentModel.Type] {
16+
[TrackItem.self]
4917
}
5018

51-
init(_ item: TrackItem) {
52-
self.id = UUID()
53-
self.type = item.type
54-
self.desc = item.desc
55-
self.tags = item.tags
56-
self.startDate = item.startDate
57-
self.endDate = item.endDate
58-
self.previousId = item.previousId
19+
@Model
20+
public final class TrackItem: Identifiable {
21+
@Attribute(.unique) public var id: UUID
22+
public var type: TrackType.RawValue = TrackType.none.rawValue
23+
public var desc: String = ""
24+
public var tags: [String] = []
25+
public var startDate: Date = Date.now
26+
public var endDate: Date = Date.now
27+
public var previousId: UUID?
28+
29+
init(
30+
type: TrackType = TrackType.none,
31+
desc: String = "",
32+
tags: [String] = [],
33+
startDate: Date = Date.now,
34+
endDate: Date = Date.now,
35+
previousId: UUID? = nil
36+
) {
37+
self.id = UUID()
38+
self.type = type.rawValue
39+
self.desc = desc
40+
self.tags = tags
41+
self.startDate = startDate
42+
self.endDate = endDate
43+
self.previousId = previousId ?? nil
44+
}
45+
46+
init(_ item: TrackItem) {
47+
self.id = UUID()
48+
self.type = item.type
49+
self.desc = item.desc
50+
self.tags = item.tags
51+
self.startDate = item.startDate
52+
self.endDate = item.endDate
53+
self.previousId = item.previousId
54+
}
5955
}
6056
}
6157

58+
public typealias TrackItem = PasiteaSchemaV1.TrackItem
59+
6260
extension TrackItem {
6361
@Transient
6462
public var typeAsTrackType: TrackType {
@@ -82,67 +80,87 @@ extension TrackItem {
8280
}
8381

8482
extension TrackItem {
83+
@discardableResult
8584
public func addTags(_ tags: String...) -> TrackItem {
8685
for tag in tags {
8786
self.tags.append(tag)
8887
}
8988
return self
9089
}
9190

92-
public func saveInto(_ modelContext: ModelContext, _ endDate: Date? = nil) {
93-
if !saved {
94-
self.saved = true
95-
self.endDate = endDate ?? self.endDate
96-
modelContext.insert(self)
97-
do {
98-
try modelContext.save()
99-
} catch {
100-
#if DEBUG
101-
print(error.localizedDescription)
102-
#endif
103-
}
104-
}
105-
}
106-
107-
public func deleteFrom(_ modelContext: ModelContext) {
108-
if !deleted {
109-
modelContext.delete(self)
110-
self.deleted = true
111-
do {
112-
try modelContext.save()
113-
} catch {
114-
#if DEBUG
115-
print(error.localizedDescription)
116-
#endif
117-
}
118-
}
119-
}
120-
91+
@discardableResult
12192
public func to(_ type: TrackType) -> TrackItem {
12293
self.type = type.rawValue
12394
print(self.type)
12495
return self
12596
}
12697

98+
@discardableResult
12799
public func startsAt(_ date: Date) -> TrackItem {
128100
self.startDate = date
129101
return self
130102
}
131103

104+
@discardableResult
132105
public func endsAt(_ date: Date) -> TrackItem {
133106
self.endDate = date
134107
return self
135108
}
136109

110+
@discardableResult
137111
public func startsNow() -> TrackItem {
138112
let date = Date.now
139113
self.startDate = date
140114
self.endDate = date
141115
return self
142116
}
143117

118+
@discardableResult
144119
public func endsNow() -> TrackItem {
145120
self.endDate = Date.now
146121
return self
147122
}
148123
}
124+
125+
@MainActor
126+
struct PersistenceFacade {
127+
let modelContext: ModelContext
128+
129+
func save(_ item: TrackItem, endDate: Date? = nil) {
130+
if let endDate {
131+
item.endDate = endDate
132+
}
133+
modelContext.insert(item)
134+
do {
135+
try modelContext.save()
136+
} catch {
137+
#if DEBUG
138+
print(error.localizedDescription)
139+
#endif
140+
}
141+
}
142+
143+
func delete(_ item: TrackItem) {
144+
modelContext.delete(item)
145+
do {
146+
try modelContext.save()
147+
} catch {
148+
#if DEBUG
149+
print(error.localizedDescription)
150+
#endif
151+
}
152+
}
153+
154+
func delete(_ items: [TrackItem]) {
155+
for item in items {
156+
modelContext.delete(item)
157+
}
158+
do {
159+
try modelContext.save()
160+
} catch {
161+
#if DEBUG
162+
print(error.localizedDescription)
163+
#endif
164+
}
165+
}
166+
}

Pasitea/Model/TrackItemWrapper.swift

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,26 @@
66
//
77

88
import Foundation
9-
import SwiftData
109

11-
class TrackItemWrapper {
10+
@MainActor
11+
final class TrackItemWrapper {
1212
private var item: TrackItem
1313

1414
init(_ item: TrackItem) {
1515
self.item = item
1616
}
1717

18-
public func reset(_ modelContext: ModelContext, tags: String...) -> TrackItemWrapper {
18+
@discardableResult
19+
public func reset(_ persistence: PersistenceFacade, tags: String...) -> TrackItemWrapper {
1920
for tag in tags {
2021
self.item.addTags(tag)
2122
}
22-
self.item.endsNow().saveInto(modelContext)
23+
persistence.save(self.item.endsNow())
2324
self.item = TrackItem(type: self.item.typeAsTrackType, previousId: self.item.previousId ?? self.item.id)
2425
return self
2526
}
2627

28+
@discardableResult
2729
public func startsNow(_ tags: String...) -> TrackItemWrapper {
2830
self.item.startsNow()
2931
for tag in tags {
@@ -32,15 +34,16 @@ class TrackItemWrapper {
3234
return self
3335
}
3436

37+
@discardableResult
3538
public func endsNow(_ tags: String...) -> TrackItemWrapper {
36-
self.item.startsNow()
39+
self.item.endsNow()
3740
for tag in tags {
3841
self.item.addTags(tag)
3942
}
4043
return self
4144
}
4245

43-
public func saveInto(_ modelContext: ModelContext) {
44-
self.item.saveInto(modelContext)
46+
public func save(_ persistence: PersistenceFacade) {
47+
persistence.save(self.item)
4548
}
4649
}

Pasitea/PasiteaApp.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@
88
import SwiftData
99
import SwiftUI
1010

11+
enum PasiteaMigrationPlan: SchemaMigrationPlan {
12+
static var schemas: [any VersionedSchema.Type] {
13+
[PasiteaSchemaV1.self]
14+
}
15+
16+
static var stages: [MigrationStage] {
17+
[]
18+
}
19+
}
20+
1121
@main
1222
struct PasiteaApp: App {
1323
@State private var modelData = ModelData()
@@ -16,7 +26,7 @@ struct PasiteaApp: App {
1626

1727
init() {
1828
do {
19-
container = try ModelContainer(for: TrackItem.self)
29+
container = try ModelContainer(for: TrackItem.self, migrationPlan: PasiteaMigrationPlan.self)
2030
} catch {
2131
#if DEBUG
2232
print(error.localizedDescription)

Pasitea/View/Calm/Calm - 5 Steps/CalmStepsView.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ struct CalmStepsView: View {
6767
Group {
6868
Text(
6969
"""
70-
The 5-4-3-2-1 method is a scientifically proven quick anxiety and stress reduction technique.
70+
The 5-4-3-2-1 method is a scientifically proven quick anxiety and stress reduction \
71+
technique.
7172
7273
To calm down:
7374

Pasitea/View/Calm/Calm - Breathe/BreatheFlowerAnimation.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
//
77

88
import Combine
9+
import SwiftData
910
import SwiftUI
1011

1112
struct BreatheFlowerAnimation: View {
12-
@Environment(\.modelContext) var modelContext
13+
@Environment(\.modelContext) var modelContext: ModelContext
14+
private var persistence: PersistenceFacade { PersistenceFacade(modelContext: modelContext) }
1315

1416
@State private var isRunning = false
1517
@State private var isDone = false
@@ -63,21 +65,26 @@ struct BreatheFlowerAnimation: View {
6365
// Middle Left Petal - Second
6466
Image("flower")
6567
.rotationEffect(.degrees(isFlowerOpen ? -25 : -5), anchor: .bottom)
68+
.accessibilityHidden(true)
6669

6770
// Middle Right Petal - Fourth
6871
Image("flower")
6972
.rotationEffect(.degrees(isFlowerOpen ? 25 : 5), anchor: .bottom)
73+
.accessibilityHidden(true)
7074

7175
// Middle Petal - Third
7276
Image("flower")
77+
.accessibilityHidden(true)
7378

7479
// Left Petal - First
7580
Image("flower")
7681
.rotationEffect(.degrees(isFlowerOpen ? -50 : -10), anchor: .bottom)
82+
.accessibilityHidden(true)
7783

7884
// Right Petal - Fifth
7985
Image("flower")
8086
.rotationEffect(.degrees(isFlowerOpen ? 50 : 10), anchor: .bottom)
87+
.accessibilityHidden(true)
8188
}
8289
.shadow(radius: isFlowerOpen ? 20 : 0)
8390
.hueRotation(Angle(degrees: isFlowerOpen ? -170 : 0))
@@ -124,7 +131,7 @@ struct BreatheFlowerAnimation: View {
124131
if isRunning {
125132
trackItem.startsNow()
126133
} else {
127-
trackItem.reset(modelContext, tags: breatheCount.description)
134+
trackItem.reset(persistence, tags: breatheCount.description)
128135
}
129136

130137
withAnimation(flowerAnimation) {
@@ -172,7 +179,7 @@ struct BreatheFlowerAnimation: View {
172179
}
173180
.onDisappear {
174181
if isRunning {
175-
trackItem.endsNow().saveInto(modelContext)
182+
trackItem.endsNow().save(persistence)
176183
}
177184
}
178185
}

0 commit comments

Comments
 (0)