diff --git a/Examples/Relationships/RelationshipsExample/RelationshipsExample/CoreDataStack.swift b/Examples/Relationships/RelationshipsExample/RelationshipsExample/CoreDataStack.swift index a36de66..334b688 100644 --- a/Examples/Relationships/RelationshipsExample/RelationshipsExample/CoreDataStack.swift +++ b/Examples/Relationships/RelationshipsExample/RelationshipsExample/CoreDataStack.swift @@ -8,9 +8,58 @@ import CoreData import Foundation +import OSLog -public enum CoreDataStack { - private static let model: NSManagedObjectModel = { +public final class CoreDataStack { + + public static let shared = CoreDataStack() + + private let logger: Logger + public let container: NSPersistentContainer + + /// The file URL for the store + /// + /// We will only ever use one store. + public func url() -> URL? { + container.persistentStoreDescriptions.first?.url + } + + public func destroy() throws { + if let url = url() { + logger.debug("Destroying store at \(url.absoluteString)") + try container.persistentStoreCoordinator.destroyPersistentStore(at: url, type: NSPersistentStore.StoreType.sqlite) + } else { + logger.debug("No URL found for store. Removing store from coordinator.") + try container.persistentStoreCoordinator + .remove(container.persistentStoreCoordinator.persistentStores.first!) + } + } + + public static func persistentContainer( + model: NSManagedObjectModel, + logger: Logger + ) -> NSPersistentContainer { + logger.info("Creating container") + let description: NSPersistentStoreDescription + logger.info("Container located at \(storeDefaultLocation.absoluteString)") + description = NSPersistentStoreDescription(url: storeDefaultLocation) + + description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) + + description.type = NSSQLiteStoreType + description.shouldAddStoreAsynchronously = false + let container = NSPersistentContainer(name: "CoreDataModel", managedObjectModel: model) + container.persistentStoreDescriptions = [description] + container.loadPersistentStores { _, error in + if let error { + fatalError("Unable to load persistent stores: \(error)") + } + } + container.viewContext.automaticallyMergesChangesFromParent = true + return container + } + + public static let defaultModel: NSManagedObjectModel = { let model = NSManagedObjectModel() model.entities = [ FileCabinet.Managed.entityDescription, @@ -20,20 +69,20 @@ public enum CoreDataStack { return model }() - public static func persistentContainer() -> NSPersistentContainer { - let desc = NSPersistentStoreDescription() - desc.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) - desc.type = NSSQLiteStoreType - desc.shouldAddStoreAsynchronously = false - let model = Self.model - let container = NSPersistentContainer(name: "Model", managedObjectModel: model) - container.persistentStoreDescriptions = [desc] - container.loadPersistentStores { _, error in - if let error { - fatalError("Unable to load persistent stores: \(error)") - } - } - container.viewContext.automaticallyMergesChangesFromParent = true - return container + public static let storeDefaultDirectory: URL = FileManager.default.urls( + for: .applicationSupportDirectory, + in: .userDomainMask + ).first! + + public static let storeDefaultLocation: URL = storeDefaultDirectory.appendingPathComponent("repository.sqlite") + + public static let storeWalDefaultLocation: URL = storeDefaultDirectory.appendingPathComponent("repository.sqlite-wal") + + public static let storeShmDefaultLocation: URL = storeDefaultDirectory.appendingPathComponent("repository.sqlite-shm") + + public init() { + let _logger = Logger(subsystem: "com.CoreDataRepository.RelationshipsExample", category: "CoreDataStack") + self.logger = _logger + self.container = Self.persistentContainer(model: Self.defaultModel, logger: _logger) } } diff --git a/Examples/Relationships/RelationshipsExample/RelationshipsExample/FileCabinetDetailView.swift b/Examples/Relationships/RelationshipsExample/RelationshipsExample/FileCabinetDetailView.swift index 6eb8398..0b54f20 100644 --- a/Examples/Relationships/RelationshipsExample/RelationshipsExample/FileCabinetDetailView.swift +++ b/Examples/Relationships/RelationshipsExample/RelationshipsExample/FileCabinetDetailView.swift @@ -16,44 +16,37 @@ struct FileCabinetDetailView: View { let viewModel: FileCabinetDetailViewModel var body: some View { - NavigationSplitView( - sidebar: sidebar, - content: content, - detail: detail - ) - } - - @ViewBuilder @MainActor - private func sidebar() -> some View { - VStack { - Text(viewModel.state.fileCabinet.id.uuidString) - Button( - action: { - Task { - await viewModel.newDrawer() + Section { + List(viewModel.state.fileCabinet.drawers) { drawer in + Text(drawer.id.uuidString) + .swipeActions { + Button(role: .destructive) { + Task { + await viewModel.delete(drawer: drawer) + } + } label: { + Label("Delete", systemImage: "trash.fill") + } } - }, - label: { - Text("+") - } - ) - List { - ForEach(viewModel.state.fileCabinet.drawers) { drawer in - Text(drawer.id.uuidString) - } } .refreshable(action: viewModel.refreshFileCabinet) + } header: { + HStack { + Text("Drawers") + Button( + action: { + Task { + await viewModel.newDrawer() + } + }, + label: { + Image(systemName: "plus") + .padding() + } + ) + } } - } - - @ViewBuilder @MainActor - private func content() -> some View { - EmptyView() - } - - @ViewBuilder @MainActor - private func detail() -> some View { - EmptyView() + .navigationTitle("File Cabinet \(viewModel.state.fileCabinet.id.uuidString)") } } @@ -93,6 +86,19 @@ final class FileCabinetDetailViewModel { print(error.localizedDescription) } } + + @Sendable + func delete(drawer: FileCabinet.Drawer) async { + guard let url = drawer.managedIdUrl else { + return + } + switch await repository.delete(url) { + case .success: + return + case let .failure(error): + print("Failed to delete drawer \(drawer.id.uuidString): \(error.localizedDescription)") + } + } private static let fetchRequest: NSFetchRequest = { let request = FileCabinet.Managed.fetchRequest() diff --git a/Examples/Relationships/RelationshipsExample/RelationshipsExample/FileCabinetsView.swift b/Examples/Relationships/RelationshipsExample/RelationshipsExample/FileCabinetsView.swift index 68ad082..f04847f 100644 --- a/Examples/Relationships/RelationshipsExample/RelationshipsExample/FileCabinetsView.swift +++ b/Examples/Relationships/RelationshipsExample/RelationshipsExample/FileCabinetsView.swift @@ -16,16 +16,36 @@ struct FileCabinetsView: View { let viewModel: FileCabinetsViewModel var body: some View { - NavigationSplitView( - sidebar: sidebar, - content: content, - detail: detail - ) + NavigationSplitView(columnVisibility: .constant(.all)) { + sidebar() + } content: { + content() + } detail: { + detail() + } } @ViewBuilder @MainActor private func sidebar() -> some View { - VStack { + List(viewModel.state.fileCabinets) { fileCabinet in + Text(fileCabinet.id.uuidString) + .onTapGesture { + Task { + await viewModel.select(fileCabinet: fileCabinet) + } + } + .swipeActions { + Button(role: .destructive) { + Task { + await viewModel.delete(fileCabinet: fileCabinet) + } + } label: { + Label("Delete", systemImage: "trash.fill") + } + } + } + .refreshable(action: viewModel.loadFileCabinets) + .toolbar { Button( action: { Task { @@ -33,21 +53,12 @@ struct FileCabinetsView: View { } }, label: { - Text("+") + Image(systemName: "plus") + .padding() } ) - List { - ForEach(viewModel.state.fileCabinets) { fileCabinet in - Text(fileCabinet.id.uuidString) - .onTapGesture { - Task { - await viewModel.select(fileCabinet: fileCabinet) - } - } - } - } - .refreshable(action: viewModel.loadFileCabinets) } + .navigationTitle("File Cabinets") } @ViewBuilder @MainActor @@ -104,6 +115,19 @@ final class FileCabinetsViewModel { detailViewModel = detail await detail.refreshFileCabinet() } + + @Sendable + func delete(fileCabinet: FileCabinet) async { + guard let url = fileCabinet.managedIdUrl else { + return + } + switch await repository.delete(url) { + case .success: + return + case let .failure(error): + print("Failed to delete file cabinet \(fileCabinet.id.uuidString): \(error.localizedDescription)") + } + } private static let fetchRequest: NSFetchRequest = { let request = FileCabinet.Managed.fetchRequest() diff --git a/Examples/Relationships/RelationshipsExample/RelationshipsExample/RelationshipsExampleApp.swift b/Examples/Relationships/RelationshipsExample/RelationshipsExample/RelationshipsExampleApp.swift index c9d09cd..2281e97 100644 --- a/Examples/Relationships/RelationshipsExample/RelationshipsExample/RelationshipsExampleApp.swift +++ b/Examples/Relationships/RelationshipsExample/RelationshipsExample/RelationshipsExampleApp.swift @@ -13,7 +13,7 @@ import SwiftUI struct RelationshipsExampleApp: App { let fileCabinetsViewModel = FileCabinetsViewModel(repository: CoreDataRepository( - context: CoreDataStack.persistentContainer() + context: CoreDataStack.shared.container .newBackgroundContext() ))