Files
InvestmentTrackerApp/PortfolioJournal/Repositories/GoalRepository.swift
T
alexandrev-tibco 773da6800b Release 1.3.1 (build 20): iCloud sync fix + sources search/filter
iCloud sync:
- Fix: deploy CloudKit schema to production so exports work
- Fix: PredictionCache marked syncable=NO (binary attr caused partial failures)
- Fix: forceExportToiCloud uses createdAt+1ms for guaranteed persistent history
- Fix: auto-deduplication on CloudKit import (processRemoteChanges)
- Add: NSPersistentHistoryTrackingKey always enabled regardless of CloudKit state
- Add: cloudKitForceReload notification so repositories re-fetch on remote changes

Sources UI:
- Add: horizontal category filter chips in SourceListView
- Add: search toggle button with animated TextField

Other:
- Add: ITSAppUsesNonExemptEncryption=NO in Info.plist (skip manual compliance)
- Add: fastlane submit lane with run_precheck_before_submit=false
- Update: release notes all locales for 1.3.1
- Update: fastlane API key via pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 22:19:33 +02:00

144 lines
4.5 KiB
Swift

import Foundation
import CoreData
import Combine
@MainActor
class GoalRepository: ObservableObject {
private let context: NSManagedObjectContext
@Published private(set) var goals: [Goal] = []
private var cancellables = Set<AnyCancellable>()
init(context: NSManagedObjectContext = CoreDataStack.shared.viewContext) {
self.context = context
fetchGoals()
setupNotificationObserver()
}
private func setupNotificationObserver() {
NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: context)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.fetchGoals()
}
.store(in: &cancellables)
NotificationCenter.default.publisher(for: .cloudKitForceReload)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in self?.fetchGoals() }
.store(in: &cancellables)
}
// MARK: - Fetch
func fetchGoals(for account: Account? = nil) {
let request: NSFetchRequest<Goal> = Goal.fetchRequest()
if let account = account {
request.predicate = NSPredicate(format: "account == %@", account)
}
request.sortDescriptors = [
NSSortDescriptor(keyPath: \Goal.createdAt, ascending: true)
]
// Performance: Add batch size for fetches
request.fetchBatchSize = 50
do {
goals = try context.fetch(request)
} catch {
print("Failed to fetch goals: \(error)")
goals = []
}
}
// MARK: - Create
/// Check if a goal with the given name already exists in the account (case-insensitive)
func goalExists(name: String, in account: Account?) -> Bool {
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
let normalized = trimmed.lowercased()
return goals.contains { goal in
let nameMatches = goal.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == normalized
let accountMatches = goal.account?.id == account?.id
return nameMatches && accountMatches
}
}
/// Find an existing goal by name (case-insensitive) within the same account
func findGoal(byName name: String, in account: Account?) -> Goal? {
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
let normalized = trimmed.lowercased()
return goals.first { goal in
let nameMatches = goal.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == normalized
let accountMatches = goal.account?.id == account?.id
return nameMatches && accountMatches
}
}
@discardableResult
func createGoal(
name: String,
targetAmount: Decimal,
targetDate: Date? = nil,
account: Account?
) -> Goal {
// Check if goal with same name already exists in this account
if let existing = findGoal(byName: name, in: account) {
return existing
}
let goal = Goal(context: context)
goal.name = name.trimmingCharacters(in: .whitespacesAndNewlines)
goal.targetAmount = NSDecimalNumber(decimal: targetAmount)
goal.targetDate = targetDate
goal.account = account
save()
return goal
}
// MARK: - Update
func updateGoal(
_ goal: Goal,
name: String? = nil,
targetAmount: Decimal? = nil,
targetDate: Date? = nil,
clearTargetDate: Bool = false,
isActive: Bool? = nil
) {
if let name = name {
goal.name = name
}
if let targetAmount = targetAmount {
goal.targetAmount = NSDecimalNumber(decimal: targetAmount)
}
if clearTargetDate {
goal.targetDate = nil
} else if let targetDate = targetDate {
goal.targetDate = targetDate
}
if let isActive = isActive {
goal.isActive = isActive
}
save()
}
func deleteGoal(_ goal: Goal) {
context.delete(goal)
save()
}
// MARK: - Save
private func save() {
guard context.hasChanges else { return }
do {
try context.save()
// Note: Removed redundant fetchGoals() call - the NotificationCenter observer
// in setupNotificationObserver() already handles refetching on context changes
} catch {
print("Failed to save goals: \(error)")
}
}
}