From b442c13719d612575905829fa71c494cb3a208d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Sun, 15 Feb 2026 23:11:40 +0100 Subject: [PATCH] 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 --- VoiceDiary.xcodeproj/project.pbxproj | 2 ++ VoiceDiary/Models/DiaryEntry.swift | 20 +++++++++++-------- VoiceDiary/Models/VoiceMemo.swift | 8 ++++---- VoiceDiary/ViewModels/DiaryViewModel.swift | 4 ++-- .../ViewModels/RecordingViewModel.swift | 3 ++- VoiceDiary/Views/ContentView.swift | 2 +- VoiceDiary/Views/DiaryEntryView.swift | 6 +++--- VoiceDiaryTests/VoiceDiaryTests.swift | 3 ++- project.yml | 1 + 9 files changed, 29 insertions(+), 20 deletions(-) diff --git a/VoiceDiary.xcodeproj/project.pbxproj b/VoiceDiary.xcodeproj/project.pbxproj index 6909079..3b15afe 100644 --- a/VoiceDiary.xcodeproj/project.pbxproj +++ b/VoiceDiary.xcodeproj/project.pbxproj @@ -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"; diff --git a/VoiceDiary/Models/DiaryEntry.swift b/VoiceDiary/Models/DiaryEntry.swift index 3b38d21..1286759 100644 --- a/VoiceDiary/Models/DiaryEntry.swift +++ b/VoiceDiary/Models/DiaryEntry.swift @@ -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 { diff --git a/VoiceDiary/Models/VoiceMemo.swift b/VoiceDiary/Models/VoiceMemo.swift index 37d4591..24837ca 100644 --- a/VoiceDiary/Models/VoiceMemo.swift +++ b/VoiceDiary/Models/VoiceMemo.swift @@ -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) { diff --git a/VoiceDiary/ViewModels/DiaryViewModel.swift b/VoiceDiary/ViewModels/DiaryViewModel.swift index 0fc4ed9..5fa7ebc 100644 --- a/VoiceDiary/ViewModels/DiaryViewModel.swift +++ b/VoiceDiary/ViewModels/DiaryViewModel.swift @@ -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 } diff --git a/VoiceDiary/ViewModels/RecordingViewModel.swift b/VoiceDiary/ViewModels/RecordingViewModel.swift index e5fc6a8..1e4d1a3 100644 --- a/VoiceDiary/ViewModels/RecordingViewModel.swift +++ b/VoiceDiary/ViewModels/RecordingViewModel.swift @@ -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() diff --git a/VoiceDiary/Views/ContentView.swift b/VoiceDiary/Views/ContentView.swift index 1b56aaa..865221e 100644 --- a/VoiceDiary/Views/ContentView.swift +++ b/VoiceDiary/Views/ContentView.swift @@ -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" ) diff --git a/VoiceDiary/Views/DiaryEntryView.swift b/VoiceDiary/Views/DiaryEntryView.swift index dc5d1a3..4de14e4 100644 --- a/VoiceDiary/Views/DiaryEntryView.swift +++ b/VoiceDiary/Views/DiaryEntryView.swift @@ -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", diff --git a/VoiceDiaryTests/VoiceDiaryTests.swift b/VoiceDiaryTests/VoiceDiaryTests.swift index 0b12c69..aaee1ee 100644 --- a/VoiceDiaryTests/VoiceDiaryTests.swift +++ b/VoiceDiaryTests/VoiceDiaryTests.swift @@ -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) } } diff --git a/project.yml b/project.yml index 3d57b23..14e07d4 100644 --- a/project.yml +++ b/project.yml @@ -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: