fix CloudKit compatibility: default values, optional relationship, background mode

- Add default values to all SwiftData attributes (CloudKit requirement)
- Make memos relationship optional (CloudKit requirement)
- Add allMemos computed property for safe unwrapping
- Add remote-notification background mode for CloudKit push sync

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 23:11:40 +01:00
parent c64f6627cb
commit b442c13719
9 changed files with 29 additions and 20 deletions

View File

@@ -291,6 +291,7 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Voice Diary needs microphone access to record your voice memos for diary entries.";
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "Voice Diary uses on-device speech recognition to transcribe your voice memos.";
INFOPLIST_KEY_UIBackgroundModes = "remote-notification";
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
@@ -374,6 +375,7 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Voice Diary needs microphone access to record your voice memos for diary entries.";
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "Voice Diary uses on-device speech recognition to transcribe your voice memos.";
INFOPLIST_KEY_UIBackgroundModes = "remote-notification";
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";

View File

@@ -3,13 +3,13 @@ import SwiftData
@Model
final class DiaryEntry {
var date: Date
var date: Date = Date.now
var summary: String?
var isSummaryGenerated: Bool
var isSummaryGenerated: Bool = false
@Relationship(deleteRule: .cascade, inverse: \VoiceMemo.entry)
var memos: [VoiceMemo]
var createdAt: Date
var updatedAt: Date
var memos: [VoiceMemo]? = []
var createdAt: Date = Date.now
var updatedAt: Date = Date.now
init(date: Date) {
self.date = Calendar.current.startOfDay(for: date)
@@ -20,19 +20,23 @@ final class DiaryEntry {
self.updatedAt = .now
}
var allMemos: [VoiceMemo] {
memos ?? []
}
var combinedTranscript: String {
memos
allMemos
.sorted { $0.recordedAt < $1.recordedAt }
.compactMap(\.transcript)
.joined(separator: "\n\n")
}
var hasMemos: Bool {
!memos.isEmpty
!allMemos.isEmpty
}
var hasTranscripts: Bool {
memos.contains { $0.transcript != nil }
allMemos.contains { $0.transcript != nil }
}
var formattedDate: String {

View File

@@ -3,11 +3,11 @@ import SwiftData
@Model
final class VoiceMemo {
var audioFileName: String
var audioFileName: String = ""
var transcript: String?
var isTranscribing: Bool
var duration: TimeInterval
var recordedAt: Date
var isTranscribing: Bool = false
var duration: TimeInterval = 0
var recordedAt: Date = Date.now
var entry: DiaryEntry?
init(audioFileName: String, duration: TimeInterval) {

View File

@@ -27,7 +27,7 @@ final class DiaryViewModel {
func deleteEntry(_ entry: DiaryEntry, context: ModelContext) {
// Delete audio files
for memo in entry.memos {
for memo in entry.allMemos {
try? FileManager.default.removeItem(at: memo.audioURL)
}
context.delete(entry)
@@ -35,7 +35,7 @@ final class DiaryViewModel {
func deleteMemo(_ memo: VoiceMemo, from entry: DiaryEntry, context: ModelContext) {
try? FileManager.default.removeItem(at: memo.audioURL)
entry.memos.removeAll { $0.persistentModelID == memo.persistentModelID }
entry.memos?.removeAll { $0.persistentModelID == memo.persistentModelID }
context.delete(memo)
entry.updatedAt = .now
}

View File

@@ -42,7 +42,8 @@ final class RecordingViewModel {
)
context.insert(memo)
memo.entry = entry
entry.memos.append(memo)
if entry.memos == nil { entry.memos = [] }
entry.memos?.append(memo)
entry.updatedAt = .now
try? context.save()

View File

@@ -80,7 +80,7 @@ struct DiaryListRow: View {
HStack(spacing: 12) {
Label(
String(localized: "diary.memoCount \(entry.memos.count)"),
String(localized: "diary.memoCount \(entry.allMemos.count)"),
systemImage: "waveform"
)

View File

@@ -96,11 +96,11 @@ struct DiaryEntryView: View {
private var memosTab: some View {
List {
ForEach(entry.memos.sorted(by: { $0.recordedAt < $1.recordedAt })) { memo in
ForEach(entry.allMemos.sorted(by: { $0.recordedAt < $1.recordedAt })) { memo in
MemoRow(memo: memo)
}
.onDelete { indexSet in
let sorted = entry.memos.sorted { $0.recordedAt < $1.recordedAt }
let sorted = entry.allMemos.sorted { $0.recordedAt < $1.recordedAt }
for index in indexSet {
viewModel.deleteMemo(sorted[index], from: entry, context: modelContext)
}
@@ -108,7 +108,7 @@ struct DiaryEntryView: View {
}
.listStyle(.plain)
.overlay {
if entry.memos.isEmpty {
if entry.allMemos.isEmpty {
ContentUnavailableView(
String(localized: "memos.empty.title"),
systemImage: "mic.slash",

View File

@@ -23,6 +23,7 @@ struct DiaryEntryTests {
entry.memos = [memo1, memo2]
#expect(entry.allMemos.count == 2)
#expect(entry.combinedTranscript.contains("First memo."))
#expect(entry.combinedTranscript.contains("Second memo."))
}
@@ -38,7 +39,7 @@ struct DiaryEntryTests {
let entry = DiaryEntry(date: .now)
#expect(!entry.hasMemos)
entry.memos.append(VoiceMemo(audioFileName: "test.m4a", duration: 5))
entry.memos = [VoiceMemo(audioFileName: "test.m4a", duration: 5)]
#expect(entry.hasMemos)
}
}

View File

@@ -36,6 +36,7 @@ targets:
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad: "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"
INFOPLIST_KEY_NSMicrophoneUsageDescription: "Voice Diary needs microphone access to record your voice memos for diary entries."
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription: "Voice Diary uses on-device speech recognition to transcribe your voice memos."
INFOPLIST_KEY_UIBackgroundModes: remote-notification
entitlements:
path: VoiceDiary/VoiceDiary.entitlements
properties: