I create a NSPersistentContainer like this:
static let persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "MyUselessApp")
container.loadPersistentStores(completionHandler: { (nsPersistentStoreDescription, error) in
guard let error = error else {
// Everything went well
return
}
fatalError(error.localizedDescription)
})
return container
}()
It loads all data/objects that are saved. Is it possible to create a NSPersistentContainer without any data, but still holds all the entities that I defined in MyUselessApp.xcdatamodeld?
I want to do this because I have a UIViewController with a NSFetchResultController, but everytime the UIViewController will be presented, the data needs to be refreshed (I can not cache anything). The current way I am doing it, is that I delete every object that the NSPredicate will return from the NSFetchResultController. That looks like a useless step to me. All my tableviews that contain dynamic data use NSFetchResultController and I prefer to keep using it this way.
Turns out this was the only thing I needed to do:
let persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "InMemoryCoreDataContainer")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
lazy var mockPersistantContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "InMemoryCoreDataContainer", managedObjectModel: persistentContainer.managedObjectModel)
let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
description.shouldAddStoreAsynchronously = false // Make it simpler in test env
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { (description, error) in
precondition( description.type == NSInMemoryStoreType )
if let error = error {
fatalError("Create an in-mem coordinator failed \(error)")
}
}
return container
}()
Related
I just added App Groups to my app using this StackOverFlow post. Unfortunately since the defaultDirectoryURL is changing, I can not fetch any of the old data I made before using the App Groups directory. I know the data is still there because when I switch back to using a regular NSPersistentContainer instead of the GroupedPersistentContainer, I can get the data.
How can I migrate my old data over to where I'm fetching the app group's data?
Core Data code:
class CoreDataManager {
static let sharedManager = CoreDataManager()
private init() {}
lazy var persistentContainer: NSPersistentContainer = {
var useCloudSync = UserDefaults.standard.bool(forKey: "useCloudSync")
let containerToUse: NSPersistentContainer?
if useCloudSync {
containerToUse = NSPersistentCloudKitContainer(name: "App")
} else {
containerToUse = GroupedPersistentContainer(name: "App")
let description = containerToUse!.persistentStoreDescriptions.first
description?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
}
guard let container = containerToUse, let description = container.persistentStoreDescriptions.first else {
fatalError("Hey Listen! ###\(#function): Failed to retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
...
return container
}
}
GroupedPersistentContainer code:
class GroupedPersistentContainer: NSPersistentContainer {
enum URLStrings: String {
case group = "group.App"
}
override class func defaultDirectoryURL() -> URL {
let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: URLStrings.group.rawValue)
if !FileManager.default.fileExists(atPath: url!.path) {
try? FileManager.default.createDirectory(at: url!, withIntermediateDirectories: true, attributes: nil)
}
return url!
}
}
I haven't done this yet for my NSPersistentCloudKitContainer yet but it will follow the same format as this one.
I would like to test my core data methode.
There is multiples entities in my coredataModel and for each I have a NSManagedObject class
there is methode inside those classes to add, delete and remove data of the corresponding entity.
public class StoredGame: NSManagedObject {
static private let storage = DataManager.shared.storage
static var all: [Game] {
let request: NSFetchRequest<StoredGame> = StoredGame.fetchRequest()
guard let storedGame = try? storage.viewContext.fetch(request) else { return [] }
var games: [Game] = .init()
storedGame.forEach { (storedGame) in
games.append(convert(storedGame))
}
return games
}
static func add(new game: Game) {
let entity = NSEntityDescription.entity(forEntityName: "StoredGame", in: storage.viewContext)!
let newGame = StoredGame(entity: entity, insertInto: storage.viewContext)
try? storage.saveContext()
}
}
and then I have a class responsible of the core data stack
class CoreDataManager {
private lazy var persistentContainer: NSPersistentContainer! = {
guard let modelURL = Bundle.main.url(forResource: "CoreData", withExtension:"momd") else {
fatalError("Error loading model from bundle")
}
// The managed object model for the application. It is a fatal error for the application not to be able to find and load its model.
guard let mom = NSManagedObjectModel(contentsOf: modelURL) else {
fatalError("Error initializing mom from: \(modelURL)")
}
let container = NSPersistentContainer(name: "CoreData", managedObjectModel: mom)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
var viewContext: NSManagedObjectContext {
return persistentContainer.viewContext
}
func saveContext () throws {
let context = viewContext
if context.hasChanges {
do {
try context.save()
} catch(let error) {
print(error)
}
}
}
}
Then when it goes to the tests. I've created a mockContainer and a mockCoreData
lazy var mockContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "CoreData")
container.persistentStoreDescriptions[0].url = URL(fileURLWithPath: "/dev/null")
container.loadPersistentStores(completionHandler: { (_, error) in
XCTAssertNil(error)
})
return container
}()
lazy var mockCoreData = {
return StoredGame(context: mockContainer.viewContext)
}()
So now I dont know how to run tests in that configuration, I've tried a
XCTAssert(StoredGame.all.isEmpty) for exemple ( i have a all var in the StoredEntity class)
but it fails with an error telling
'NSInvalidArgumentException', reason: '-[CoreData.StoredEntity setId:]: unrecognized selector sent to instance
any idea?
This might be occurring with passing an invalid URL for the store description. Unless you need to run tests with a NSSQLiteStoreType, which is the default for NSPersistentContainer, you may want to consider using an NSInMemoryStoreType for unit testing. A small tweak to your CoreDataManager class could allow you to initialize the class both for your app and unit tests. For example:
class CoreDataManager {
private let persisted: Bool
init(persisted: Bool = true) {
self.persisted = persisted
}
lazy var persistentContainer: NSPersistentContainer = {
let description = NSPersistentStoreDescription()
if persisted {
description.type = NSSQLiteStoreType
description.url = // location to store the db.
} else {
description.type = NSInMemoryStoreType
}
let container = NSPersistentContainer(name: "CoreData")
container.persistentStoreDescriptions = [description]
container.loadPersistentStores //...
return container
}()
}
Then you can use this exact class in your unit tests without need to create a mock. Just initialize it without persistence:
let manager = CoreDataManager(persisted: false)
I'm trying to create a singleton class which works with an NSManagedObjectContext.
This is the class:
import Foundation
import CoreData
class PersistenceService{
init(){}
// MARK: - Core Data stack
static var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
static var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "frazeit")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
static func saveContext () {
let mainContext = persistentContainer.viewContext
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.parent = mainContext
privateContext.perform {
if privateContext.hasChanges {
do {
try privateContext.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
}
In some occasion, it does not push changes into the persistent store, while the app is open the persistent container is changed but when I re-run the app changes are gone. What's the right way to save the changes into the persistent store.
This the class that does not work properly:
class func add(word: String, quotes:[Quotes], language: String){
for item in quotes {
if let phrase = item.phrase, let author = item.author {
let quote = CachedQuotes(context: PersistenceService.context)
quote.phrase = phrase
quote.date = Date() as NSDate
quote.keyword = word
quote.language = language
quote.author = author
PersistenceService.saveContext()
}
}
}
I call it to save quotes which are fetched from the network:
override func viewDidLoad() {
let quotes = CachedQuotes.getAllQuotes()
//Prints the number of saved records which is 0 now
self.getQuote { (result, error) in
if let qoutes = result?.quotes {
CachedQuotes.add(word: "friend", quotes: qoutes, language: "en")
}
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let quotes = CachedQuotes.getAllQuotes()
//Prints the number of saved records which is 10 now
}
But when I re-run the app, nothing is saved into the persistance container.
UPDATE:
The code below works now
static func saveContext () {
let mainContext = persistentContainer.viewContext
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.automaticallyMergesChangesFromParent = true
privateContext.parent = mainContext
privateContext.perform {
do {
try privateContext.save()
mainContext.perform({
do {
try mainContext.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
})
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
First it saves the private quoue then saves the main.
let mainContext = persistentContainer.viewContext
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.parent = mainContext
You edit a context and then save the same context to persist the changes. Creating a child context to .viewContext and saving said child context does not save the .viewContext itself, where you made changes.
If you want to use background queues, first set var automaticallyMergesChangesFromParent: Bool on the .viewContext where you want to receive changes from the background queue. Then you create a background context, set on it the same persistentStoreCoordinator from .viewContext, make changes on it and then save the background queue.
Using privateContext.perform is a good start. You can do better if you wrap the changes to quote in a perform through the context in which the quote was created in the first place, so you access quote through the same thread the context uses.
Here is the singleton from Apple's Refreshing and Maintaining Your App Using Background Tasks sample.
import Foundation
import CoreData
class PersistentContainer: NSPersistentContainer {
private static let lastCleanedKey = "lastCleaned"
static let shared: PersistentContainer = {
ValueTransformer.setValueTransformer(ColorTransformer(), forName: NSValueTransformerName(rawValue: String(describing: ColorTransformer.self)))
let container = PersistentContainer(name: "ColorFeed")
container.loadPersistentStores { (desc, error) in
if let error = error {
fatalError("Unresolved error \(error)")
}
print("Successfully loaded persistent store at: \(desc.url?.description ?? "nil")")
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyStoreTrumpMergePolicyType)
return container
}()
var lastCleaned: Date? {
get {
return UserDefaults.standard.object(forKey: PersistentContainer.lastCleanedKey) as? Date
}
set {
UserDefaults.standard.set(newValue, forKey: PersistentContainer.lastCleanedKey)
}
}
override func newBackgroundContext() -> NSManagedObjectContext {
let backgroundContext = super.newBackgroundContext()
backgroundContext.automaticallyMergesChangesFromParent = true
backgroundContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyStoreTrumpMergePolicyType)
return backgroundContext
}
}
Personally I prefer passing the NSPersistentContainer around via dependency injection but it requires a lot more effort.
I want to build a signup form using core data in swift 4 .
However,I am getting an error: "Unexpectedly found nil while unwrapping an Optional value"
#IBAction func registerfunction(_ sender: Any) {
let userEmail = usernamefield.text;
let userPassword = passwordfield.text;
let userConfirmPassword = confirmfield.text;
let city = cityfield.text;
let dateofbirth = dateofbirthfield.text;
let gender = genderfield.text;
var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "User")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error {
fatalError("Unresolved error, \((error as NSError).userInfo)")
}
})
return container
}()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let managedObjectContext = appDelegate.persistentContainer.viewContext as! NSManagedObjectContext
let context = managedObjectContext
let newUser = NSEntityDescription.insertNewObject(forEntityName: "User", into: context) as NSManagedObject
newUser.setValue(usernamefield.text, forKey: "name")
newUser.setValue(passwordfield.text, forKey: "password")
newUser.setValue(confirmfield.text, forKey: "confirmpassword")
newUser.setValue(genderfield.text, forKey: "gender")
do {
try context.save()
} catch {}
print(newUser)
print("Object Saved.")
And here is my app delegate code :
import UIKit
import CoreData
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "User")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error {
fatalError("Unresolved error, \((error as NSError).userInfo)")
}
})
return container
}()
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
Can you please help me? Thanks in advance
I imported CoreData,and I am using the file Users.xcdatamodeld
I use this code to initialize Core Data:
import Cocoa
import CoreData
class DataController: NSObject {
var persistentContainer: NSPersistentContainer!
var context: NSManagedObjectContext!
override init() {
persistentContainer = NSPersistentContainer(name: "Highlightings")
persistentContainer.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
context = persistentContainer.viewContext
}
}
How can I specify a .sqlite file where Core Data should save data?
You could go back to the older approach without NSPersistentContainer. The old API has not been deprecated.
If you're using NSPersistentContainer, you can change the store location using NSPersistentStoreDescription. Something like
let container = NSPersistentContainer(name: "ContinerName")
let storeURL = // Initialize to whatever URL you want
let description = NSPersistentStoreDescription(url: storeURL)
container.persistentStoreDescriptions = [ description ]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
// ...
}