Primera build enviada
This commit is contained in:
@@ -2,11 +2,14 @@ import Foundation
|
||||
import CoreData
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
class AccountRepository: ObservableObject {
|
||||
private let context: NSManagedObjectContext
|
||||
|
||||
@Published private(set) var accounts: [Account] = []
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(context: NSManagedObjectContext = CoreDataStack.shared.viewContext) {
|
||||
self.context = context
|
||||
fetchAccounts()
|
||||
@@ -14,40 +17,30 @@ class AccountRepository: ObservableObject {
|
||||
}
|
||||
|
||||
private func setupNotificationObserver() {
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(contextDidChange),
|
||||
name: .NSManagedObjectContextObjectsDidChange,
|
||||
object: context
|
||||
)
|
||||
}
|
||||
|
||||
@objc private func contextDidChange(_ notification: Notification) {
|
||||
fetchAccounts()
|
||||
NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: context)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
self?.fetchAccounts()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: - Fetch
|
||||
|
||||
func fetchAccounts() {
|
||||
context.perform { [weak self] in
|
||||
guard let self else { return }
|
||||
let request: NSFetchRequest<Account> = Account.fetchRequest()
|
||||
request.sortDescriptors = [
|
||||
NSSortDescriptor(keyPath: \Account.sortOrder, ascending: true),
|
||||
NSSortDescriptor(keyPath: \Account.createdAt, ascending: true)
|
||||
]
|
||||
let request: NSFetchRequest<Account> = Account.fetchRequest()
|
||||
request.sortDescriptors = [
|
||||
NSSortDescriptor(keyPath: \Account.sortOrder, ascending: true),
|
||||
NSSortDescriptor(keyPath: \Account.createdAt, ascending: true)
|
||||
]
|
||||
// Performance: Add batch size for fetches
|
||||
request.fetchBatchSize = 50
|
||||
|
||||
do {
|
||||
let fetched = try self.context.fetch(request)
|
||||
DispatchQueue.main.async {
|
||||
self.accounts = fetched
|
||||
}
|
||||
} catch {
|
||||
print("Failed to fetch accounts: \(error)")
|
||||
DispatchQueue.main.async {
|
||||
self.accounts = []
|
||||
}
|
||||
}
|
||||
do {
|
||||
accounts = try context.fetch(request)
|
||||
} catch {
|
||||
print("Failed to fetch accounts: \(error)")
|
||||
accounts = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,30 +73,85 @@ class AccountRepository: ObservableObject {
|
||||
}
|
||||
|
||||
func createDefaultAccountIfNeeded() -> Account {
|
||||
if let existing = accounts.first {
|
||||
// Fetch accounts directly from database to avoid race condition with async fetch
|
||||
let request: NSFetchRequest<Account> = Account.fetchRequest()
|
||||
request.sortDescriptors = [
|
||||
NSSortDescriptor(keyPath: \Account.sortOrder, ascending: true),
|
||||
NSSortDescriptor(keyPath: \Account.createdAt, ascending: true)
|
||||
]
|
||||
let existingAccounts = (try? context.fetch(request)) ?? []
|
||||
|
||||
// Check if Default account already exists
|
||||
if let defaultAccount = existingAccounts.first(where: { $0.isDefaultAccount }) {
|
||||
return defaultAccount
|
||||
}
|
||||
|
||||
// If no Default account but other accounts exist, return first one
|
||||
if let existing = existingAccounts.first {
|
||||
return existing
|
||||
}
|
||||
|
||||
// No accounts exist, create Default account
|
||||
let defaultCurrency = AppSettings.getOrCreate(in: context).currency
|
||||
let account = createAccount(
|
||||
name: "Personal",
|
||||
currency: defaultCurrency,
|
||||
inputMode: .simple,
|
||||
notificationFrequency: .monthly
|
||||
)
|
||||
let account = Account(context: context)
|
||||
account.name = Account.defaultAccountName
|
||||
account.currency = defaultCurrency
|
||||
account.inputMode = InputMode.simple.rawValue
|
||||
account.notificationFrequency = NotificationFrequency.monthly.rawValue
|
||||
account.customFrequencyMonths = 1
|
||||
account.sortOrder = 0
|
||||
|
||||
// Attach existing sources to the default account
|
||||
let request: NSFetchRequest<InvestmentSource> = InvestmentSource.fetchRequest()
|
||||
if let sources = try? context.fetch(request) {
|
||||
let sourceRequest: NSFetchRequest<InvestmentSource> = InvestmentSource.fetchRequest()
|
||||
if let sources = try? context.fetch(sourceRequest) {
|
||||
for source in sources where source.account == nil {
|
||||
source.account = account
|
||||
}
|
||||
save()
|
||||
}
|
||||
|
||||
save()
|
||||
return account
|
||||
}
|
||||
|
||||
/// Removes duplicate Default accounts, keeping only the oldest one.
|
||||
/// Call this once to clean up any duplicates created by the race condition bug.
|
||||
func cleanupDuplicateDefaultAccounts() {
|
||||
let request: NSFetchRequest<Account> = Account.fetchRequest()
|
||||
request.predicate = NSPredicate(format: "name == %@", Account.defaultAccountName)
|
||||
request.sortDescriptors = [NSSortDescriptor(keyPath: \Account.createdAt, ascending: true)]
|
||||
|
||||
guard let defaultAccounts = try? context.fetch(request), defaultAccounts.count > 1 else {
|
||||
return
|
||||
}
|
||||
|
||||
// Keep the first (oldest) Default account, delete the rest
|
||||
let accountsToDelete = defaultAccounts.dropFirst()
|
||||
for account in accountsToDelete {
|
||||
// Move sources to the kept Default account before deleting
|
||||
if let keptAccount = defaultAccounts.first {
|
||||
for source in account.sourcesArray {
|
||||
source.account = keptAccount
|
||||
}
|
||||
}
|
||||
context.delete(account)
|
||||
}
|
||||
|
||||
save()
|
||||
}
|
||||
|
||||
// MARK: - Validation
|
||||
|
||||
func isNameAvailable(_ name: String, excludingAccountId: UUID? = nil) -> Bool {
|
||||
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let normalized = trimmed.lowercased()
|
||||
return !accounts.contains { account in
|
||||
if let excludeId = excludingAccountId, account.safeId == excludeId {
|
||||
return false
|
||||
}
|
||||
return account.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == normalized
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Update
|
||||
|
||||
func updateAccount(
|
||||
@@ -134,7 +182,15 @@ class AccountRepository: ObservableObject {
|
||||
|
||||
// MARK: - Delete
|
||||
|
||||
func canDeleteAccount(_ account: Account) -> Bool {
|
||||
// Cannot delete the Default account
|
||||
guard !account.isDefaultAccount else { return false }
|
||||
// Must keep at least one account
|
||||
return accounts.count > 1
|
||||
}
|
||||
|
||||
func deleteAccount(_ account: Account) {
|
||||
guard canDeleteAccount(account) else { return }
|
||||
context.delete(account)
|
||||
save()
|
||||
}
|
||||
@@ -145,7 +201,8 @@ class AccountRepository: ObservableObject {
|
||||
guard context.hasChanges else { return }
|
||||
do {
|
||||
try context.save()
|
||||
fetchAccounts()
|
||||
// Note: Removed redundant fetchAccounts() call - the NotificationCenter observer
|
||||
// in setupNotificationObserver() already handles refetching on context changes
|
||||
} catch {
|
||||
print("Failed to save accounts: \(error)")
|
||||
}
|
||||
|
||||
@@ -2,53 +2,47 @@ import Foundation
|
||||
import CoreData
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
class CategoryRepository: ObservableObject {
|
||||
private let context: NSManagedObjectContext
|
||||
|
||||
@Published private(set) var categories: [Category] = []
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(context: NSManagedObjectContext = CoreDataStack.shared.viewContext) {
|
||||
self.context = context
|
||||
fetchCategories()
|
||||
ensureDefaultCategoriesExist()
|
||||
setupNotificationObserver()
|
||||
}
|
||||
|
||||
private func setupNotificationObserver() {
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(contextDidChange),
|
||||
name: .NSManagedObjectContextObjectsDidChange,
|
||||
object: context
|
||||
)
|
||||
}
|
||||
|
||||
@objc private func contextDidChange(_ notification: Notification) {
|
||||
guard isRelevantChange(notification) else { return }
|
||||
fetchCategories()
|
||||
NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: context)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] notification in
|
||||
guard let self, self.isRelevantChange(notification) else { return }
|
||||
self.fetchCategories()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: - Fetch
|
||||
|
||||
func fetchCategories() {
|
||||
context.perform { [weak self] in
|
||||
guard let self else { return }
|
||||
let request: NSFetchRequest<Category> = Category.fetchRequest()
|
||||
request.sortDescriptors = [
|
||||
NSSortDescriptor(keyPath: \Category.sortOrder, ascending: true),
|
||||
NSSortDescriptor(keyPath: \Category.name, ascending: true)
|
||||
]
|
||||
let request: NSFetchRequest<Category> = Category.fetchRequest()
|
||||
request.sortDescriptors = [
|
||||
NSSortDescriptor(keyPath: \Category.sortOrder, ascending: true),
|
||||
NSSortDescriptor(keyPath: \Category.name, ascending: true)
|
||||
]
|
||||
// Performance: Add batch size for fetches
|
||||
request.fetchBatchSize = 50
|
||||
|
||||
do {
|
||||
let fetched = try self.context.fetch(request)
|
||||
DispatchQueue.main.async {
|
||||
self.categories = fetched
|
||||
}
|
||||
} catch {
|
||||
print("Failed to fetch categories: \(error)")
|
||||
DispatchQueue.main.async {
|
||||
self.categories = []
|
||||
}
|
||||
}
|
||||
do {
|
||||
categories = try context.fetch(request)
|
||||
} catch {
|
||||
print("Failed to fetch categories: \(error)")
|
||||
categories = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,14 +55,33 @@ class CategoryRepository: ObservableObject {
|
||||
|
||||
// MARK: - Create
|
||||
|
||||
/// Check if a category with the given name already exists (case-insensitive)
|
||||
func categoryExists(name: String) -> Bool {
|
||||
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let normalized = trimmed.lowercased()
|
||||
return categories.contains { $0.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == normalized }
|
||||
}
|
||||
|
||||
/// Find an existing category by name (case-insensitive)
|
||||
func findCategory(byName name: String) -> Category? {
|
||||
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let normalized = trimmed.lowercased()
|
||||
return categories.first { $0.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == normalized }
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func createCategory(
|
||||
name: String,
|
||||
colorHex: String,
|
||||
icon: String
|
||||
) -> Category {
|
||||
// Check if category with same name already exists
|
||||
if let existing = findCategory(byName: name) {
|
||||
return existing
|
||||
}
|
||||
|
||||
let category = Category(context: context)
|
||||
category.name = name
|
||||
category.name = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
category.colorHex = colorHex
|
||||
category.icon = icon
|
||||
category.sortOrder = Int16(categories.count)
|
||||
@@ -78,8 +91,29 @@ class CategoryRepository: ObservableObject {
|
||||
}
|
||||
|
||||
func createDefaultCategoriesIfNeeded() {
|
||||
guard categories.isEmpty else { return }
|
||||
Category.createDefaultCategories(in: context)
|
||||
ensureDefaultCategoriesExist()
|
||||
}
|
||||
|
||||
func ensureDefaultCategoriesExist() {
|
||||
var addedCount = 0
|
||||
var nextSortOrder = categories.count
|
||||
|
||||
for categoryData in Category.defaultCategories {
|
||||
if categoryExists(name: categoryData.name) {
|
||||
continue
|
||||
}
|
||||
|
||||
let category = Category(context: context)
|
||||
category.name = categoryData.name
|
||||
category.colorHex = categoryData.colorHex
|
||||
category.icon = categoryData.icon
|
||||
category.sortOrder = Int16(nextSortOrder)
|
||||
nextSortOrder += 1
|
||||
addedCount += 1
|
||||
}
|
||||
|
||||
guard addedCount > 0 else { return }
|
||||
save()
|
||||
fetchCategories()
|
||||
}
|
||||
|
||||
@@ -148,7 +182,8 @@ class CategoryRepository: ObservableObject {
|
||||
guard context.hasChanges else { return }
|
||||
do {
|
||||
try context.save()
|
||||
fetchCategories()
|
||||
// Note: Removed redundant fetchCategories() call - the NotificationCenter observer
|
||||
// in setupNotificationObserver() already handles refetching on context changes
|
||||
CoreDataStack.shared.refreshWidgetData()
|
||||
} catch {
|
||||
print("Failed to save context: \(error)")
|
||||
|
||||
@@ -2,11 +2,14 @@ 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()
|
||||
@@ -14,47 +17,59 @@ class GoalRepository: ObservableObject {
|
||||
}
|
||||
|
||||
private func setupNotificationObserver() {
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(contextDidChange),
|
||||
name: .NSManagedObjectContextObjectsDidChange,
|
||||
object: context
|
||||
)
|
||||
}
|
||||
|
||||
@objc private func contextDidChange(_ notification: Notification) {
|
||||
fetchGoals()
|
||||
NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: context)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
self?.fetchGoals()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: - Fetch
|
||||
|
||||
func fetchGoals(for account: Account? = nil) {
|
||||
context.perform { [weak self] in
|
||||
guard let self else { return }
|
||||
let request: NSFetchRequest<Goal> = Goal.fetchRequest()
|
||||
if let account = account {
|
||||
request.predicate = NSPredicate(format: "account == %@", account)
|
||||
}
|
||||
request.sortDescriptors = [
|
||||
NSSortDescriptor(keyPath: \Goal.createdAt, ascending: true)
|
||||
]
|
||||
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 {
|
||||
let fetched = try self.context.fetch(request)
|
||||
DispatchQueue.main.async {
|
||||
self.goals = fetched
|
||||
}
|
||||
} catch {
|
||||
print("Failed to fetch goals: \(error)")
|
||||
DispatchQueue.main.async {
|
||||
self.goals = []
|
||||
}
|
||||
}
|
||||
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,
|
||||
@@ -62,8 +77,13 @@ class GoalRepository: ObservableObject {
|
||||
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
|
||||
goal.name = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
goal.targetAmount = NSDecimalNumber(decimal: targetAmount)
|
||||
goal.targetDate = targetDate
|
||||
goal.account = account
|
||||
@@ -109,7 +129,8 @@ class GoalRepository: ObservableObject {
|
||||
guard context.hasChanges else { return }
|
||||
do {
|
||||
try context.save()
|
||||
fetchGoals()
|
||||
// Note: Removed redundant fetchGoals() call - the NotificationCenter observer
|
||||
// in setupNotificationObserver() already handles refetching on context changes
|
||||
} catch {
|
||||
print("Failed to save goals: \(error)")
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@ import Foundation
|
||||
import CoreData
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
class InvestmentSourceRepository: ObservableObject {
|
||||
private let context: NSManagedObjectContext
|
||||
|
||||
@Published private(set) var sources: [InvestmentSource] = []
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(context: NSManagedObjectContext = CoreDataStack.shared.viewContext) {
|
||||
self.context = context
|
||||
fetchSources()
|
||||
@@ -14,43 +17,33 @@ class InvestmentSourceRepository: ObservableObject {
|
||||
}
|
||||
|
||||
private func setupNotificationObserver() {
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(contextDidChange),
|
||||
name: .NSManagedObjectContextObjectsDidChange,
|
||||
object: context
|
||||
)
|
||||
}
|
||||
|
||||
@objc private func contextDidChange(_ notification: Notification) {
|
||||
guard isRelevantChange(notification) else { return }
|
||||
fetchSources()
|
||||
NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: context)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] notification in
|
||||
guard let self, self.isRelevantChange(notification) else { return }
|
||||
self.fetchSources()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: - Fetch
|
||||
|
||||
func fetchSources(account: Account? = nil) {
|
||||
context.perform { [weak self] in
|
||||
guard let self else { return }
|
||||
let request: NSFetchRequest<InvestmentSource> = InvestmentSource.fetchRequest()
|
||||
if let account = account {
|
||||
request.predicate = NSPredicate(format: "account == %@", account)
|
||||
}
|
||||
request.sortDescriptors = [
|
||||
NSSortDescriptor(keyPath: \InvestmentSource.name, ascending: true)
|
||||
]
|
||||
let request: NSFetchRequest<InvestmentSource> = InvestmentSource.fetchRequest()
|
||||
if let account = account {
|
||||
request.predicate = NSPredicate(format: "account == %@", account)
|
||||
}
|
||||
request.sortDescriptors = [
|
||||
NSSortDescriptor(keyPath: \InvestmentSource.name, ascending: true)
|
||||
]
|
||||
// Performance: Add batch size for large datasets
|
||||
request.fetchBatchSize = 100
|
||||
|
||||
do {
|
||||
let fetched = try self.context.fetch(request)
|
||||
DispatchQueue.main.async {
|
||||
self.sources = fetched
|
||||
}
|
||||
} catch {
|
||||
print("Failed to fetch sources: \(error)")
|
||||
DispatchQueue.main.async {
|
||||
self.sources = []
|
||||
}
|
||||
}
|
||||
do {
|
||||
sources = try context.fetch(request)
|
||||
} catch {
|
||||
print("Failed to fetch sources: \(error)")
|
||||
sources = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +75,28 @@ class InvestmentSourceRepository: ObservableObject {
|
||||
|
||||
// MARK: - Create
|
||||
|
||||
/// Check if a source with the given name already exists in the account (case-insensitive)
|
||||
func sourceExists(name: String, in account: Account?) -> Bool {
|
||||
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let normalized = trimmed.lowercased()
|
||||
return sources.contains { source in
|
||||
let nameMatches = source.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == normalized
|
||||
let accountMatches = source.account?.id == account?.id
|
||||
return nameMatches && accountMatches
|
||||
}
|
||||
}
|
||||
|
||||
/// Find an existing source by name (case-insensitive) within the same account
|
||||
func findSource(byName name: String, in account: Account?) -> InvestmentSource? {
|
||||
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let normalized = trimmed.lowercased()
|
||||
return sources.first { source in
|
||||
let nameMatches = source.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == normalized
|
||||
let accountMatches = source.account?.id == account?.id
|
||||
return nameMatches && accountMatches
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func createSource(
|
||||
name: String,
|
||||
@@ -90,8 +105,13 @@ class InvestmentSourceRepository: ObservableObject {
|
||||
customFrequencyMonths: Int = 1,
|
||||
account: Account? = nil
|
||||
) -> InvestmentSource {
|
||||
// Check if source with same name already exists in this account
|
||||
if let existing = findSource(byName: name, in: account) {
|
||||
return existing
|
||||
}
|
||||
|
||||
let source = InvestmentSource(context: context)
|
||||
source.name = name
|
||||
source.name = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
source.category = category
|
||||
source.notificationFrequency = notificationFrequency.rawValue
|
||||
source.customFrequencyMonths = Int16(customFrequencyMonths)
|
||||
@@ -188,7 +208,8 @@ class InvestmentSourceRepository: ObservableObject {
|
||||
guard context.hasChanges else { return }
|
||||
do {
|
||||
try context.save()
|
||||
fetchSources()
|
||||
// Note: Removed redundant fetchSources() call - the NotificationCenter observer
|
||||
// in setupNotificationObserver() already handles refetching on context changes
|
||||
CoreDataStack.shared.refreshWidgetData()
|
||||
} catch {
|
||||
print("Failed to save context: \(error)")
|
||||
|
||||
@@ -2,6 +2,7 @@ import Foundation
|
||||
import CoreData
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
class SnapshotRepository: ObservableObject {
|
||||
private let context: NSManagedObjectContext
|
||||
private let cache = NSCache<NSString, NSArray>()
|
||||
@@ -9,6 +10,8 @@ class SnapshotRepository: ObservableObject {
|
||||
|
||||
@Published private(set) var snapshots: [Snapshot] = []
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: - Performance: Shared DateFormatter
|
||||
private static let monthYearFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
@@ -301,16 +304,17 @@ class SnapshotRepository: ObservableObject {
|
||||
cacheVersion &+= 1
|
||||
}
|
||||
|
||||
private func setupNotificationObserver() {
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(contextDidChange),
|
||||
name: .NSManagedObjectContextObjectsDidChange,
|
||||
object: context
|
||||
)
|
||||
/// Clear cache on memory pressure - call from AppDelegate's didReceiveMemoryWarning
|
||||
func clearCacheOnMemoryPressure() {
|
||||
cache.removeAllObjects()
|
||||
}
|
||||
|
||||
@objc private func contextDidChange(_ notification: Notification) {
|
||||
invalidateCache()
|
||||
private func setupNotificationObserver() {
|
||||
NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: context)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
self?.invalidateCache()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user