Skip to content

Commit

Permalink
Automatic creation and management of the Core Data stack (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
spnkr authored Feb 22, 2023
1 parent 5628571 commit 20cea52
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 22 deletions.
22 changes: 13 additions & 9 deletions Example App/Example App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@ import CoreData

class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
let viewContext = PersistenceController.shared.container.viewContext

let backgroundContext = PersistenceController.shared.container.newBackgroundContext()
backgroundContext.automaticallyMergesChangesFromParent = true
backgroundContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy

CoreDataPlus.setup( viewContext: viewContext,
backgroundContext: backgroundContext,
logHandler: { message in print("🌎🌧 log: \(message)") }
)
// if you're not using SwiftUI, this is how to set it up in the AppDelegate
// the SwiftUI equivalent is here: Example_AppApp.swift
//
// let viewContext = PersistenceController.shared.container.viewContext
//
// let backgroundContext = PersistenceController.shared.container.newBackgroundContext()
// backgroundContext.automaticallyMergesChangesFromParent = true
// backgroundContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
//
// try! CoreDataPlus.setup( viewContext: viewContext,
// backgroundContext: backgroundContext,
// logHandler: { message in print("🌎🌧 log: \(message)") }
// )

return true
}
Expand Down
16 changes: 13 additions & 3 deletions Example App/Example App/Example_AppApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,32 @@
//

import SwiftUI
import CoreDataPlus

@main
struct Example_AppApp: App {
let persistenceController = PersistenceController.shared
let yourDataStore = CoreDataPlusStore.shared

@Environment(\.scenePhase) private var phase
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

init() {
do {
try CoreDataPlus.setup(store: CoreDataPlusStore.shared)
} catch {
NSLog(error.localizedDescription)
}
}

var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environment(\.managedObjectContext, yourDataStore.viewContext)
}
.onChange(of: phase) { newPhase in
switch newPhase {
case .background:
try! persistenceController.container.viewContext.save()
try! yourDataStore.viewContext.save()
default:
break
}
Expand Down
10 changes: 10 additions & 0 deletions Sources/CoreDataPlus/CoreDataContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

import Foundation
import CoreData
import NotificationCenter

public protocol CoreDataContainer {
/// Thread safety is up to you
var viewContext: NSManagedObjectContext { get }
var backgroundContext: NSManagedObjectContext { get }
}
28 changes: 23 additions & 5 deletions Sources/CoreDataPlus/CoreDataPlus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import Foundation
import CoreData
import NotificationCenter



public class CoreDataPlus {
public static let shared = CoreDataPlus()
internal static var config: Config?
Expand All @@ -24,10 +26,12 @@ public class CoreDataPlus {
}

public var backgroundContext: NSManagedObjectContext? {
get { CoreDataPlus.config?.backgroundContext }
get {
CoreDataPlus.config?.backgroundContext
}
}

/// <#Description#>
/// Description
/// - Parameters:
/// - viewContext: <#viewContext description#>
/// - backgroundContext: <#backgroundContext description#>
Expand All @@ -51,19 +55,33 @@ public class CoreDataPlus {
///
///
///

// for comment:
// should these setup methods be added?
// public class func setup(persistentContainer)
// public class func setup()
public class func setup(viewContext: NSManagedObjectContext,
backgroundContext: NSManagedObjectContext? = nil,
logHandler: @escaping (String) -> Void) {
logHandler: @escaping (String) -> Void) throws {

if CoreDataPlus.config != nil {
raiseError(InternalError.setupAlreadyCalled)
throw InternalError.setupAlreadyCalled
}

CoreDataPlusLogger.configure(logHandler: logHandler)
CoreDataPlus.config = Config(viewContext: viewContext, backgroundContext: backgroundContext)

}


/// One line config. Sets up core data.
public class func setup(store: CoreDataPlusStore) throws {
if CoreDataPlus.config != nil {
throw InternalError.setupAlreadyCalled
}

CoreDataPlus.config = Config(viewContext: store.viewContext, backgroundContext: store.backgroundContext)
}

private init() {
CoreDataPlusLogger.shared.log("Initializing CoreDataPlus.shared")
}
Expand Down
120 changes: 120 additions & 0 deletions Sources/CoreDataPlus/CoreDataPlusStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@

import Foundation
import CoreData
import NotificationCenter

public class CoreDataPlusStore: CoreDataContainer {
public static let shared = CoreDataPlusStore()
private init() {
modelName = guessModelName()
}

private var didConfigure = false
public var assignedModelName: String? {
modelName
}
internal var modelName: String!
// internal var overriddenPersistentContainer: NSPersistentContainer?
// internal var overriddenPersistentCloudKitContainer: NSPersistentCloudKitContainer?

/// Use this
public func configure() {
let persist = CoreDataPlusStore.shared

guard persist.didConfigure == false else {
NSLog("Already configured. Ignorning.")
return
}

persist.modelName = guessModelName()

CoreDataPlusLogger.shared.log("Enabled persistence for data model '\(String(describing: persist.modelName))' (auto-detected)")

persist.didConfigure = true
}

/// If you have a model name mistmatch:
public func configure(modelName overrideName: String?) {
let persist = CoreDataPlusStore.shared

guard persist.didConfigure == false else {
NSLog("Already configured. Ignorning.")
return
}

persist.modelName = overrideName

CoreDataPlusLogger.shared.log("Enabled persistence for data model '\(String(describing: persist.modelName))'")

persist.didConfigure = true
}

// /// To use your own NSPersistentContainer with our automatic context management:
// /// For example, to set various properties like change tracking.
// public func configure(container: NSPersistentContainer) {
// let persist = CoreDataPlusStore.shared
//
// guard persist.didConfigure == false else { return }
//
// persist.overriddenPersistentContainer = container
//
// CoreDataPlusLogger.shared.log("Enabled persistence using manual NSPersistentContainer")
//
// persist.didConfigure = true
// }
// /// To use your own NSPersistentCloudKitContainer with our automatic context management:
// /// For example, to set various properties like version history
// public func configure(cloudKitContainer: NSPersistentCloudKitContainer?) {
//
// }

/// To use your own NSManagedObjectContext management, don't use CoreDataPlusStore. Instead pass your contexts to each call.
public func configure(viewContext: NSManagedObjectContext, backgroundContext: NSManagedObjectContext?) { }

/// Read-only context for use on main thread.
public lazy var viewContext: NSManagedObjectContext = {
let c = persistentContainer.viewContext
c.automaticallyMergesChangesFromParent = true
return c
}()

/// Single background context for all writes, and background data loading.
public lazy var backgroundContext: NSManagedObjectContext = {
let newbackgroundContext = persistentContainer.newBackgroundContext()
newbackgroundContext.automaticallyMergesChangesFromParent = true
newbackgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return newbackgroundContext
}()

private func guessModelName() -> String? {
let momdUrls = Bundle.main.urls(forResourcesWithExtension: "momd", subdirectory: nil) ?? []

guard momdUrls.count == 1 else { return nil }

return momdUrls.first?.lastPathComponent.replacingOccurrences(of: ".momd", with: "")
}

lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: modelName)

container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})

return container
}()
}

2 changes: 1 addition & 1 deletion Sources/CoreDataPlus/InternalError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal enum InternalError: Error, LocalizedError {
case .noBackground:
return "No background context specified. Use .setup with a backgroundContext: parameter."
case .noForeground:
return "No foreground context (view context) specified. Use .setup with a viewContext: parameter."
return "No foreground context (view context) specified. Try using .setup with a viewContext: parameter."
default:
return "\(self)"
}
Expand Down
8 changes: 4 additions & 4 deletions Tests/CoreDataPlusTests/CoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ final class CoreTests: BaseTestCase {
c.clearAll()

CoreDataPlus.config = nil
CoreDataPlus.setup(viewContext: c, backgroundContext: b, logHandler: { _ in
try! CoreDataPlus.setup(viewContext: c, backgroundContext: b, logHandler: { _ in

})

Expand Down Expand Up @@ -124,7 +124,7 @@ final class CoreTests: BaseTestCase {
c.clearAll()

CoreDataPlus.config = nil
CoreDataPlus.setup(viewContext: c, backgroundContext: b, logHandler: { _ in
try! CoreDataPlus.setup(viewContext: c, backgroundContext: b, logHandler: { _ in

})

Expand Down Expand Up @@ -180,7 +180,7 @@ final class CoreTests: BaseTestCase {
c.clearAll()

CoreDataPlus.config = nil
CoreDataPlus.setup(viewContext: c, backgroundContext: b, logHandler: { _ in
try! CoreDataPlus.setup(viewContext: c, backgroundContext: b, logHandler: { _ in

})

Expand Down Expand Up @@ -221,7 +221,7 @@ final class CoreTests: BaseTestCase {
c.clearAll()

CoreDataPlus.config = nil
CoreDataPlus.setup(viewContext: c, backgroundContext: b, logHandler: { _ in
try! CoreDataPlus.setup(viewContext: c, backgroundContext: b, logHandler: { _ in

})

Expand Down

0 comments on commit 20cea52

Please sign in to comment.