From e640ab99b78ca1b3d9477237f9d99b102f8625e9 Mon Sep 17 00:00:00 2001 From: Belli Date: Sat, 25 Apr 2026 14:21:57 -0700 Subject: [PATCH 1/5] refactor(services): replace force-unwraps in service layer with safe forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cleans up 3 force_unwrapping violations in: - Managers/CameraManager.swift:93 - Utilities/OpenAIClient.swift:26 - Utilities/SignInWithAppleHelper.swift:236 Each site uses the smallest fix that matches its semantics β€” guard let with early return, if let, ?? default, or guard let + fatalError when an invariant guarantees non-nil. No swiftlint:disable directives added. Part 1 of 4 in docs/plans/maintenance-cleanup.md Task 3 cleanup. --- Savely/Managers/CameraManager.swift | 9 +++++---- Savely/Utilities/OpenAIClient.swift | 7 +++++-- Savely/Utilities/SignInWithAppleHelper.swift | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Savely/Managers/CameraManager.swift b/Savely/Managers/CameraManager.swift index 671d5ec..152d012 100755 --- a/Savely/Managers/CameraManager.swift +++ b/Savely/Managers/CameraManager.swift @@ -87,10 +87,11 @@ class CameraManager: NSObject, ObservableObject { } func setPreviewLayer(to view: UIView) { - previewLayer = AVCaptureVideoPreviewLayer(session: session) - previewLayer?.videoGravity = .resizeAspectFill - previewLayer?.frame = view.bounds - view.layer.insertSublayer(previewLayer!, at: 0) + let layer = AVCaptureVideoPreviewLayer(session: session) + layer.videoGravity = .resizeAspectFill + layer.frame = view.bounds + previewLayer = layer + view.layer.insertSublayer(layer, at: 0) } func capturePhoto() { diff --git a/Savely/Utilities/OpenAIClient.swift b/Savely/Utilities/OpenAIClient.swift index e20a7e5..4e2ef3e 100644 --- a/Savely/Utilities/OpenAIClient.swift +++ b/Savely/Utilities/OpenAIClient.swift @@ -23,8 +23,11 @@ class OpenAIClient { } func fetchTip(prompt: String) async -> String? { - let url = URL(string: "https://api.openai.com/v1/chat/completions")! - + guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else { + return nil + } + + // Construir el mensaje para el modelo let messages = [ OpenAIChatMessage(role: "system", content: "Eres un asistente financiero."), diff --git a/Savely/Utilities/SignInWithAppleHelper.swift b/Savely/Utilities/SignInWithAppleHelper.swift index 67c332c..a5e7530 100644 --- a/Savely/Utilities/SignInWithAppleHelper.swift +++ b/Savely/Utilities/SignInWithAppleHelper.swift @@ -233,6 +233,6 @@ extension SignInWithAppleHelper: ASAuthorizationControllerDelegate { extension UIViewController: ASAuthorizationControllerPresentationContextProviding { public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - return self.view.window! + return self.view.window ?? ASPresentationAnchor() } } From a53346cd748fdb8ec3a37da9e5713e9435604d85 Mon Sep 17 00:00:00 2001 From: Belli Date: Sat, 25 Apr 2026 14:27:39 -0700 Subject: [PATCH 2/5] chore: fix broken pre-commit hook to actually run SwiftLint The --use-script-input-files flag reads from Xcode build-phase env vars (SCRIPT_INPUT_FILE_*), not stdin. The heredoc <<< was a no-op and every CLI git commit failed with "SCRIPT_INPUT_FILE_COUNT variable not set". Pass each staged file as a positional argument instead. Discovered while running the Task 3 force-unwrap cleanup. --- .githooks/pre-commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 58c7131..4578b26 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -24,7 +24,7 @@ echo "πŸ” Running SwiftLint on staged files…" # Lint only the staged files (faster than full project) echo "$staged_swift_files" | while read -r file; do if [[ -f "$file" ]]; then - swiftlint lint --quiet --strict --use-script-input-files <<< "$file" || { + swiftlint lint --quiet --strict "$file" || { echo "" echo "❌ SwiftLint failed. Fix the issues above, then re-stage and commit." echo " To bypass (NOT recommended), use: git commit --no-verify" From 7935d15c73b45362a406b925ec7ff24bfb78c846 Mon Sep 17 00:00:00 2001 From: Belli Date: Sat, 25 Apr 2026 14:31:17 -0700 Subject: [PATCH 3/5] refactor(models): replace force-unwraps in IncomeModel and ExpenseModel Cleans up 2 force_unwrapping violations in: - Models/IncomeModel.swift:32 - Models/ExpenseModel.swift:32 Applied the fix inline in each file rather than introducing a shared helper, since deduplicating a single line across two files would have required adding a third file and a project.pbxproj edit. Part 2 of 4 in docs/plans/maintenance-cleanup.md Task 3 cleanup. --- Savely/Models/ExpenseModel.swift | 6 +++++- Savely/Models/IncomeModel.swift | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Savely/Models/ExpenseModel.swift b/Savely/Models/ExpenseModel.swift index 9d9cbf0..9a4dcd0 100755 --- a/Savely/Models/ExpenseModel.swift +++ b/Savely/Models/ExpenseModel.swift @@ -29,7 +29,11 @@ extension ExpenseModel { print("Fetching expenses from \(startDate) to \(endDate)") // Precompute adjusted end date - let adjustedEndDate = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: endDate)!) + guard let nextDay = Calendar.current.date(byAdding: .day, value: 1, to: endDate) else { + print("Error computing adjusted end date for expense fetch") + return [] + } + let adjustedEndDate = Calendar.current.startOfDay(for: nextDay) let fetchDescriptor = FetchDescriptor( predicate: #Predicate { diff --git a/Savely/Models/IncomeModel.swift b/Savely/Models/IncomeModel.swift index 35c12bf..4558762 100644 --- a/Savely/Models/IncomeModel.swift +++ b/Savely/Models/IncomeModel.swift @@ -29,7 +29,11 @@ extension IncomeModel { print("Fetching incomes from \(startDate) to \(endDate)") // Precompute adjusted end date - let adjustedEndDate = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: endDate)!) + guard let nextDay = Calendar.current.date(byAdding: .day, value: 1, to: endDate) else { + print("Error computing adjusted end date for income fetch") + return [] + } + let adjustedEndDate = Calendar.current.startOfDay(for: nextDay) let fetchDescriptor = FetchDescriptor( predicate: #Predicate { From b13e472ab77fe7d03776e6737a15e0b9d2eb236a Mon Sep 17 00:00:00 2001 From: Belli Date: Sat, 25 Apr 2026 15:03:38 -0700 Subject: [PATCH 4/5] refactor(views): replace force-unwraps in viewmodels and view layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cleans up 5 force_unwrapping violations in: - ViewModels/ProfileTab/ProfileViewModel.swift:125, 142 - ViewModels/Dashboard/ReportsViewModel.swift:77, 92 - Views/MainNavigationView.swift:659 Each site uses the smallest fix that matches its semantics β€” guard let with early return, if let, ?? default. No swiftlint:disable directives. Part 3 of 4 in docs/plans/maintenance-cleanup.md Task 3 cleanup. --- Savely/ViewModels/Dashboard/ReportsViewModel.swift | 6 ++++-- Savely/ViewModels/ProfileTab/ProfileViewModel.swift | 6 ++++-- Savely/Views/MainNavigationView.swift | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Savely/ViewModels/Dashboard/ReportsViewModel.swift b/Savely/ViewModels/Dashboard/ReportsViewModel.swift index d4fb318..722b57c 100644 --- a/Savely/ViewModels/Dashboard/ReportsViewModel.swift +++ b/Savely/ViewModels/Dashboard/ReportsViewModel.swift @@ -74,7 +74,8 @@ class ReportsViewModel: ObservableObject { print("Fetching weekly incomes...") let adjustedStartDate = Calendar.current.startOfDay(for: startDate) - let adjustedEndDate = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: endDate)!) + guard let nextDay = Calendar.current.date(byAdding: .day, value: 1, to: endDate) else { return [] } + let adjustedEndDate = Calendar.current.startOfDay(for: nextDay) let fetchDescriptor = FetchDescriptor( predicate: #Predicate { @@ -89,7 +90,8 @@ class ReportsViewModel: ObservableObject { print("Fetching weekly expenses...") let adjustedStartDate = Calendar.current.startOfDay(for: startDate) - let adjustedEndDate = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: endDate)!) + guard let nextDay = Calendar.current.date(byAdding: .day, value: 1, to: endDate) else { return [] } + let adjustedEndDate = Calendar.current.startOfDay(for: nextDay) let fetchDescriptor = FetchDescriptor( predicate: #Predicate { diff --git a/Savely/ViewModels/ProfileTab/ProfileViewModel.swift b/Savely/ViewModels/ProfileTab/ProfileViewModel.swift index 1bfd847..1dfe2e8 100644 --- a/Savely/ViewModels/ProfileTab/ProfileViewModel.swift +++ b/Savely/ViewModels/ProfileTab/ProfileViewModel.swift @@ -122,7 +122,8 @@ class ProfileViewModel: ObservableObject { private func fetchWeeklyIncome(from startDate: Date, to endDate: Date, in context: ModelContext) async throws -> [IncomeModel] { print("fetchWeeklyIncome: Fetching incomes...") let adjustedStartDate = Calendar.current.startOfDay(for: startDate) - let adjustedEndDate = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: endDate)!) + guard let nextDay = Calendar.current.date(byAdding: .day, value: 1, to: endDate) else { return [] } + let adjustedEndDate = Calendar.current.startOfDay(for: nextDay) let fetchDescriptor = FetchDescriptor( predicate: #Predicate { @@ -139,7 +140,8 @@ class ProfileViewModel: ObservableObject { private func fetchWeeklyExpenses(from startDate: Date, to endDate: Date, in context: ModelContext) async throws -> [ExpenseModel] { print("fetchWeeklyExpenses: Fetching expenses...") let adjustedStartDate = Calendar.current.startOfDay(for: startDate) - let adjustedEndDate = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: endDate)!) + guard let nextDay = Calendar.current.date(byAdding: .day, value: 1, to: endDate) else { return [] } + let adjustedEndDate = Calendar.current.startOfDay(for: nextDay) let fetchDescriptor = FetchDescriptor( predicate: #Predicate { diff --git a/Savely/Views/MainNavigationView.swift b/Savely/Views/MainNavigationView.swift index 3398e98..d9d62a8 100755 --- a/Savely/Views/MainNavigationView.swift +++ b/Savely/Views/MainNavigationView.swift @@ -656,7 +656,7 @@ struct WarmQuickDepositView: View { Button(action: saveDeposit) { let goalName = selectedGoal.map { $0.name.components(separatedBy: ",").first ?? $0.name } - Text(goalName != nil ? "Add $\(Int(selectedPreset)) to \(goalName!)" : "Select a goal") + Text(goalName.map { "Add $\(Int(selectedPreset)) to \($0)" } ?? "Select a goal") .font(.system(size: 15, weight: .semibold)).foregroundStyle(.white) .frame(maxWidth: .infinity).frame(height: 50) .background(selectedGoal != nil ? Color.warmGreen : Color.warmInkMuted) From aa1ae554e5f74e20e503d4399900e1607af75fde Mon Sep 17 00:00:00 2001 From: Belli Date: Sat, 25 Apr 2026 20:07:45 -0700 Subject: [PATCH 5/5] chore: flip force_unwrapping severity from warning to error The 10 force_unwrapping sites that previously triggered warnings have all been refactored in this PR to use safe forms (guard let, if let, ?? default, or fatalError for invariant-guaranteed values). Bumping severity to error means any new force-unwrap from this commit forward fails CI and the local pre-commit hook. Part 4 of 4 in docs/plans/maintenance-cleanup.md Task 3 cleanup. --- .swiftlint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 77cea0e..7aba661 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -90,7 +90,7 @@ force_cast: force_try: severity: error # error: same force_unwrapping: - severity: warning # warning today β†’ bump to error after cleanup PR + severity: error # Custom project rules custom_rules: