172 lines
4.6 KiB
Swift
172 lines
4.6 KiB
Swift
import Foundation
|
|
import CoreData
|
|
import Combine
|
|
|
|
class CategoryRepository: ObservableObject {
|
|
private let context: NSManagedObjectContext
|
|
|
|
@Published private(set) var categories: [Category] = []
|
|
|
|
init(context: NSManagedObjectContext = CoreDataStack.shared.viewContext) {
|
|
self.context = context
|
|
fetchCategories()
|
|
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()
|
|
}
|
|
|
|
// 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)
|
|
]
|
|
|
|
do {
|
|
self.categories = try self.context.fetch(request)
|
|
} catch {
|
|
print("Failed to fetch categories: \(error)")
|
|
self.categories = []
|
|
}
|
|
}
|
|
}
|
|
|
|
func fetchCategory(by id: UUID) -> Category? {
|
|
let request: NSFetchRequest<Category> = Category.fetchRequest()
|
|
request.predicate = NSPredicate(format: "id == %@", id as CVarArg)
|
|
request.fetchLimit = 1
|
|
return try? context.fetch(request).first
|
|
}
|
|
|
|
// MARK: - Create
|
|
|
|
@discardableResult
|
|
func createCategory(
|
|
name: String,
|
|
colorHex: String,
|
|
icon: String
|
|
) -> Category {
|
|
let category = Category(context: context)
|
|
category.name = name
|
|
category.colorHex = colorHex
|
|
category.icon = icon
|
|
category.sortOrder = Int16(categories.count)
|
|
|
|
save()
|
|
return category
|
|
}
|
|
|
|
func createDefaultCategoriesIfNeeded() {
|
|
guard categories.isEmpty else { return }
|
|
Category.createDefaultCategories(in: context)
|
|
fetchCategories()
|
|
}
|
|
|
|
// MARK: - Update
|
|
|
|
func updateCategory(
|
|
_ category: Category,
|
|
name: String? = nil,
|
|
colorHex: String? = nil,
|
|
icon: String? = nil
|
|
) {
|
|
if let name = name {
|
|
category.name = name
|
|
}
|
|
if let colorHex = colorHex {
|
|
category.colorHex = colorHex
|
|
}
|
|
if let icon = icon {
|
|
category.icon = icon
|
|
}
|
|
|
|
save()
|
|
}
|
|
|
|
func updateSortOrder(_ categories: [Category]) {
|
|
for (index, category) in categories.enumerated() {
|
|
category.sortOrder = Int16(index)
|
|
}
|
|
save()
|
|
}
|
|
|
|
// MARK: - Delete
|
|
|
|
func deleteCategory(_ category: Category) {
|
|
context.delete(category)
|
|
save()
|
|
}
|
|
|
|
func deleteCategory(at offsets: IndexSet) {
|
|
for index in offsets {
|
|
guard index < categories.count else { continue }
|
|
context.delete(categories[index])
|
|
}
|
|
save()
|
|
}
|
|
|
|
// MARK: - Statistics
|
|
|
|
var totalValue: Decimal {
|
|
categories.reduce(Decimal.zero) { $0 + $1.totalValue }
|
|
}
|
|
|
|
func categoryAllocation() -> [(category: Category, percentage: Double)] {
|
|
let total = totalValue
|
|
guard total > 0 else { return [] }
|
|
|
|
return categories.map { category in
|
|
let percentage = NSDecimalNumber(decimal: category.totalValue / total).doubleValue * 100
|
|
return (category: category, percentage: percentage)
|
|
}.sorted { $0.percentage > $1.percentage }
|
|
}
|
|
|
|
// MARK: - Save
|
|
|
|
private func save() {
|
|
guard context.hasChanges else { return }
|
|
do {
|
|
try context.save()
|
|
fetchCategories()
|
|
CoreDataStack.shared.refreshWidgetData()
|
|
} catch {
|
|
print("Failed to save context: \(error)")
|
|
}
|
|
}
|
|
|
|
private func isRelevantChange(_ notification: Notification) -> Bool {
|
|
guard let info = notification.userInfo else { return false }
|
|
let keys: [String] = [
|
|
NSInsertedObjectsKey,
|
|
NSUpdatedObjectsKey,
|
|
NSDeletedObjectsKey,
|
|
NSRefreshedObjectsKey
|
|
]
|
|
|
|
for key in keys {
|
|
if let objects = info[key] as? Set<NSManagedObject> {
|
|
if objects.contains(where: { $0 is Category }) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|