Using a mocked Core Data container with background processes - ios

I implemented unit testing with my Swift iOS app that uses Core Data, but I am having trouble getting unit tests to pass. I referenced the guide linked below and some stack overflow threads, but I'm still having trouble.
The app runs without flaws when actually built, but the tests fail, leading me to believe that I've messed up somewhere with integrating the mocked the Core Data container and background persistent containers. However, I can't seem to figure out where I'm going wrong here, so any advice is appreciated.
I apologize if my post is lacking any key details, but I'll add them in as necessary.
Main reference material: Cracking the Tests for Core Data
Test Code
class MealTests: XCTestCase {
var sut: MealController!
lazy var managedObjectModel: NSManagedObjectModel = {
let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle(for: type(of: self))])!
return managedObjectModel
}()
lazy var mockPersistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "WeeklyModel", managedObjectModel: self.managedObjectModel)
let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
description.shouldAddStoreAsynchronously = false
container.persistentStoreDescriptions = [description]
container.loadPersistentStores {(description, error) in
//Double check to confirm that date store is in memory
precondition(description.type == NSInMemoryStoreType)
//Check for errors
if let error = error {
fatalError("Creating an in-memory coordinator failed")
}
}
return container
}()
override func setUp() {
super.setUp()
sut = MealController(tense: .all, persistentContainer: mockPersistentContainer)
}
func test_CreatingNewMeal_IsNotNilAndPropertiesSetCorrectly() {
let mealType = Int16(1)
let date = NSDate(timeIntervalSinceNow: 0.0)
let mealDescription = "Testy McTestFace"
let servings = Int16(1)
let image = UIImage(named: "Recipe")
let favorite = false
let meal = sut.saveLocalMeal(mealType: mealType, date: date, mealDescription: mealDescription, servings: servings, image: image, favorite: favorite)
//Test meal is not nil
XCTAssertNotNil(meal, "Should not be nil")
//Testing creating meal sets all properties correctly
XCTAssertEqual(meal?.meal, mealType)
}
Controller Class
class MealController {
//MARK: - Properties
//<...>
private let persistentContainer: NSPersistentContainer!
private var fetchedResultsController: NSFetchedResultsController<Meal>!
lazy var backgroundMOC: NSManagedObjectContext = {
return self.persistentContainer.newBackgroundContext()
}()
//Set to standard persistence container by default
init(tense: Tense, persistentContainer: NSPersistentContainer) {
self.persistentContainer = persistentContainer
self.persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
reloadAllLocalData(for: tense)
}
convenience init(tense: Tense) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
fatalError("Could not set Managed Object Context")
}
self.init(tense: tense, persistentContainer: appDelegate.persistenceContainer)
}
func getMealCount() -> Int {
return fetchedResultsController.fetchedObjects?.count ?? 0
}
func getMeal(at indexPath: IndexPath) -> Meal {
return fetchedResultsController.object(at: indexPath)
}
//The boolean determines whether historical or future data is shown
func reloadAllLocalData(for tense: Tense){
let fetchRequest: NSFetchRequest<Meal> = Meal.fetchRequest()
//Setting predicate, etc.
//<...>
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: backgroundMOC, sectionNameKeyPath: nil, cacheName: nil)//Add cache to this?
do {
try fetchedResultsController.performFetch()
} catch let error {
//<...>
}
}
//Saving
func saveLocalMeal(mealType: Int16, date: NSDate, mealDescription: String, servings: Int16, image: UIImage?, favorite: Bool, comment: String? = nil, recipeUID: String? = nil) -> Meal? {
//Make new meal object
let meal = Meal(entity: Meal.entity(), insertInto: backgroundMOC)
//Call helper func and result result
return save(context: backgroundMOC, meal: meal, mealUID: nil, mealType: mealType, date: date, mealDescription: mealDescription, servings: servings, image: image, favorite: favorite, comment: comment, recipeUID: recipeUID)
}
private func save(context: NSManagedObjectContext, meal: Meal, mealUID: String?, mealType: Int16, date: NSDate, mealDescription: String, servings: Int16, image: UIImage?, favorite: Bool, comment: String? = nil, recipeUID: String? = nil) -> Meal? {
//Setting properties
//<...>
do {
try backgroundMOC.save()
return meal
} catch let error as NSError {
print("Could not save. \(error)")
return nil
}
}

Related

iOS - CoreData CloudKit sharing - participants adding children

I have implemented sharing of items from my Core Data store, using the NSPersistentCloudKitContainer api, with limited success. The objects I share (a TripsLog) have child objects (Trip), which appear on participants' devices. I've implemented more or less a direct copy of Apple's example code, except using my own core data entities.
The problem is that any new trips I create on participants' devices don't appear in the owner's database, whereas any I create on the owner's device appear for all participants, updating their displays pretty much instantly. If I edit any of the original trips on a participant's device, the changes are successfully shared, it's just the new ones which are problematic.
I'm getting a lot of noise in the console -
"Attempt to map database failed: permission was denied. This attempt will not be retried"
"Permission Failure" (10/2007); server message = "Invalid bundle ID for container"
etc
which really aren't helpful. The bundle ID & containers are set up properly and it all works fine for syncing across a single user's devices.
Although my code is more-or-less just the same as Apple's, except for the container identifier & the Core Data entities, here's most of the relevant stuff -
// sharing protocol
protocol CloudKitSharable: NSManagedObject {
static var entityName: String { get }
var identifier: String { get }
var sharedTitle: String { get }
var sharedSubject: String? { get }
var thumbnailImage: UIImage? { get }
}
class PersistenceController: NSObject {
enum CoreDataError: Error {
case modelURLNotFound(forResourceName: String)
case modelLoadingFailed(forURL: URL)
}
static let shared = PersistenceController()
private static let containerIdentifier = "iCloud.com.containerIdentifier"
private var _privatePersistentStore: NSPersistentStore?
var privatePersistentStore: NSPersistentStore {
return _privatePersistentStore!
}
private var _sharedPersistentStore: NSPersistentStore?
var sharedPersistentStore: NSPersistentStore {
return _sharedPersistentStore!
}
lazy var cloudKitContainer: CKContainer = {
return CKContainer(identifier: Self.containerIdentifier)
}()
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container: NSPersistentCloudKitContainer = try! mainDatabaseContainer()
guard let localDatabaseURL = localDatabaseURL, let cloudDatabaseURL = cloudDatabaseURL else {
fatalError("#\(#function): Failed to get local database URLs")
}
// Set up the database which will sync over the cloud
let cloudStoreDescription = NSPersistentStoreDescription(url: cloudDatabaseURL)
cloudStoreDescription.configuration = "Cloud"
cloudStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
setupStoreDescription(cloudStoreDescription)
let cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: Self.containerIdentifier)
cloudKitContainerOptions.databaseScope = .private
cloudStoreDescription.cloudKitContainerOptions = cloudKitContainerOptions
// Setting up for sharing
let sharedStoreDescription = cloudStoreDescription.copy() as! NSPersistentStoreDescription
sharedStoreDescription.url = sharedDatabaseURL
let sharedStoreOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: Self.containerIdentifier)
sharedStoreOptions.databaseScope = .shared
sharedStoreDescription.cloudKitContainerOptions = sharedStoreOptions
// Set up the stuff which we don't want to sync to the cloud
let localStoreDescription = NSPersistentStoreDescription(url: localDatabaseURL)
localStoreDescription.configuration = "Local"
setupStoreDescription(localStoreDescription)
// finish setting up the container
container.persistentStoreDescriptions = [
cloudStoreDescription,
localStoreDescription,
sharedStoreDescription
]
loadPersistentStores(for: container)
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = TransactionAuthor.app
/**
Automatically merge the changes from other contexts.
*/
container.viewContext.automaticallyMergesChangesFromParent = true
/**
Pin the viewContext to the current generation token and set it to keep itself up-to-date with local changes.
*/
do {
try container.viewContext.setQueryGenerationFrom(.current)
} catch {
fatalError("#\(#function): Failed to pin viewContext to the current generation:\(error)")
}
/**
Observe the following notifications:
- The remote change notifications from container.persistentStoreCoordinator.
- The .NSManagedObjectContextDidSave notifications from any context.
- The event change notifications from the container.
*/
NotificationCenter.default.addObserver(self, selector: #selector(storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange,
object: container.persistentStoreCoordinator)
NotificationCenter.default.addObserver(self, selector: #selector(containerEventChanged(_:)),
name: NSPersistentCloudKitContainer.eventChangedNotification,
object: container)
return container
}()
func mergeTransactions(_ transactions: [NSPersistentHistoryTransaction], to context: NSManagedObjectContext) {
context.perform {
for transaction in transactions {
context.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
}
}
}
private var cloudDatabaseURL: URL? {
return UIApplication.applicationSupportDirectory?.appendingPathComponent("CloudDatabase.sqlite")
}
private var localDatabaseURL: URL? {
return UIApplication.applicationSupportDirectory?.appendingPathComponent("Database.sqlite")
}
private var sharedDatabaseURL: URL? {
return UIApplication.applicationSupportDirectory?.appendingPathComponent("Shared.sqlite")
}
private func setupStoreDescription(_ description: NSPersistentStoreDescription) {
description.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption)
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.shouldInferMappingModelAutomatically = true
}
private func model(name: String) throws -> NSManagedObjectModel {
return try loadModel(name: name, bundle: Bundle.main)
}
private func loadModel(name: String, bundle: Bundle) throws -> NSManagedObjectModel {
guard let modelURL = bundle.url(forResource: name, withExtension: "momd") else {
throw CoreDataError.modelURLNotFound(forResourceName: name)
}
guard let model = NSManagedObjectModel(contentsOf: modelURL) else {
throw CoreDataError.modelLoadingFailed(forURL: modelURL)
}
return model
}
private func mainDatabaseContainer() throws -> NSPersistentCloudKitContainer {
return NSPersistentCloudKitContainer(name: "Database", managedObjectModel: try model(name: "Database"))
}
private func locationsContainer() throws -> NSPersistentContainer {
return NSPersistentContainer(name: "Locations", managedObjectModel: try model(name: "Locations"))
}
private func loadPersistentStores(for container: NSPersistentContainer) {
container.loadPersistentStores { [unowned self] storeDescription, error in
if let error = error {
fatalError("#\(#function): Failed to load persistent stores:\(error)")
} else {
print("Database store ok: ", storeDescription)
if let containerOptions = storeDescription.cloudKitContainerOptions, let url = self.sharedDatabaseURL {
if containerOptions.databaseScope == .shared {
let sharedStore = container.persistentStoreCoordinator.persistentStore(for: url)
self._sharedPersistentStore = sharedStore
}
} else if let url = self.cloudDatabaseURL {
let privateStore = container.persistentStoreCoordinator.persistentStore(for: url)
self._privatePersistentStore = privateStore
}
}
}
}
/**
An operation queue for handling history-processing tasks: watching changes, deduplicating tags, and triggering UI updates, if needed.
*/
lazy var historyQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
}
// Sharing extensions
extension PersistenceController {
func presentCloudSharingController<T: CloudKitSharable>(for item: T) {
/**
Grab the share if the item is already shared.
*/
var itemToShare: CKShare?
if let shareSet = try? persistentContainer.fetchShares(matching: [item.objectID]), let (_, share) = shareSet.first {
itemToShare = share
}
let sharingController: UICloudSharingController
if let itemToShare = itemToShare {
sharingController = UICloudSharingController(share: itemToShare, container: cloudKitContainer)
} else {
sharingController = newSharingController(for: item)
}
sharingController.delegate = self
/**
Setting the presentation style to .formSheet so there's no need to specify sourceView, sourceItem, or sourceRect.
*/
if let viewController = rootViewController {
sharingController.modalPresentationStyle = .formSheet
viewController.present(sharingController, animated: true)
}
}
func presentCloudSharingController(share: CKShare) {
let sharingController = UICloudSharingController(share: share, container: cloudKitContainer)
sharingController.delegate = self
/**
Setting the presentation style to .formSheet so there's no need to specify sourceView, sourceItem, or sourceRect.
*/
if let viewController = rootViewController {
sharingController.modalPresentationStyle = .formSheet
viewController.present(sharingController, animated: true)
}
}
private func newSharingController<T: CloudKitSharable>(for unsharedItem: T) -> UICloudSharingController {
return UICloudSharingController { (_, completion: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
/**
The app doesn't specify a share intentionally, so Core Data creates a new share (zone).
CloudKit has a limit on how many zones a database can have, so this app provides an option for users to use an existing share.
If the share's publicPermission is CKShareParticipantPermissionNone, only private participants can accept the share.
Private participants mean the participants an app adds to a share by calling CKShare.addParticipant.
If the share is more permissive, and is, therefore, a public share, anyone with the shareURL can accept it,
or self-add themselves to it.
The default value of publicPermission is CKShare.ParticipantPermission.none.
*/
self.persistentContainer.share([unsharedItem], to: nil) { objectIDs, share, container, error in
if let share = share {
self.configure(share: share, with: unsharedItem)
}
completion(share, container, error)
}
}
}
private var rootViewController: UIViewController? {
for scene in UIApplication.shared.connectedScenes {
if scene.activationState == .foregroundActive,
let sceneDelegate = (scene as? UIWindowScene)?.delegate as? UIWindowSceneDelegate,
let window = sceneDelegate.window {
return window?.rootViewController
}
}
print("\(#function): Failed to retrieve the window's root view controller.")
return nil
}
}
extension PersistenceController: UICloudSharingControllerDelegate {
/**
CloudKit triggers the delegate method in two cases:
- An owner stops sharing a share.
- A participant removes themselves from a share by tapping the Remove Me button in UICloudSharingController.
After stopping the sharing, purge the zone or just wait for an import to update the local store.
This sample chooses to purge the zone to avoid stale UI. That triggers a "zone not found" error because UICloudSharingController
deletes the zone, but the error doesn't really matter in this context.
Purging the zone has a caveat:
- When sharing an object from the owner side, Core Data moves the object to the shared zone.
- When calling purgeObjectsAndRecordsInZone, Core Data removes all the objects and records in the zone.
To keep the objects, deep copy the object graph you want to keep and make sure no object in the new graph is associated with any share.
The purge API posts an NSPersistentStoreRemoteChange notification after finishing its job, so observe the notification to update
the UI, if necessary.
*/
func cloudSharingControllerDidStopSharing(_ csc: UICloudSharingController) {
if let share = csc.share {
purgeObjectsAndRecords(with: share)
}
}
func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) {
if let share = csc.share, let persistentStore = share.persistentStore {
persistentContainer.persistUpdatedShare(share, in: persistentStore) { (share, error) in
if let error = error {
print("\(#function): Failed to persist updated share: \(error)")
}
}
}
}
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
print("\(#function): Failed to save a share: \(error)")
}
func itemTitle(for csc: UICloudSharingController) -> String? {
return csc.share?.title ?? "Airframe Logbook"
}
}
extension PersistenceController {
func shareObject<T: CloudKitSharable>(_ unsharedObject: T, to existingShare: CKShare?, completionHandler: ((_ share: CKShare?, _ error: Error?) -> Void)? = nil) {
persistentContainer.share([unsharedObject], to: existingShare) { (objectIDs, share, container, error) in
guard error == nil, let share = share else {
print("\(#function): Failed to share an object: \(error!))")
completionHandler?(share, error)
return
}
/**
Deduplicate tags, if necessary, because adding a photo to an existing share moves the whole object graph to the associated
record zone, which can lead to duplicated tags.
*/
if existingShare != nil {
/*
if let tagObjectIDs = objectIDs?.filter({ $0.entity.name == "Tag" }), !tagObjectIDs.isEmpty {
self.deduplicateAndWait(tagObjectIDs: Array(tagObjectIDs))
}
*/
} else {
self.configure(share: share, with: unsharedObject)
}
/**
Synchronize the changes on the share to the private persistent store.
*/
self.persistentContainer.persistUpdatedShare(share, in: self.privatePersistentStore) { (share, error) in
if let error = error {
print("\(#function): Failed to persist updated share: \(error)")
}
completionHandler?(share, error)
}
}
}
/**
Delete the Core Data objects and the records in the CloudKit record zone associated with the share.
*/
func purgeObjectsAndRecords(with share: CKShare, in persistentStore: NSPersistentStore? = nil) {
guard let store = (persistentStore ?? share.persistentStore) else {
print("\(#function): Failed to find the persistent store for share. \(share))")
return
}
persistentContainer.purgeObjectsAndRecordsInZone(with: share.recordID.zoneID, in: store) { (zoneID, error) in
if let error = error {
print("\(#function): Failed to purge objects and records: \(error)")
}
}
}
func existingShare(for item: NSManagedObject) -> CKShare? {
if let shareSet = try? persistentContainer.fetchShares(matching: [item.objectID]),
let (_, share) = shareSet.first {
return share
}
return nil
}
func share(with title: String) -> CKShare? {
let stores = [privatePersistentStore, sharedPersistentStore]
let shares = try? persistentContainer.fetchShares(in: stores)
let share = shares?.first(where: { $0.title == title })
return share
}
func shareTitles() -> [String] {
let stores = [privatePersistentStore, sharedPersistentStore]
let shares = try? persistentContainer.fetchShares(in: stores)
return shares?.map { $0.title } ?? []
}
private func configure<T: CloudKitSharable>(share: CKShare, with item: T) {
share[CKShare.SystemFieldKey.title] = item.sharedTitle
}
}
extension PersistenceController {
func addParticipant(emailAddress: String, permission: CKShare.ParticipantPermission = .readWrite, share: CKShare,
completionHandler: ((_ share: CKShare?, _ error: Error?) -> Void)?) {
/**
Use the email address to look up the participant from the private store. Return if the participant doesn't exist.
Use privatePersistentStore directly because only the owner may add participants to a share.
*/
let lookupInfo = CKUserIdentity.LookupInfo(emailAddress: emailAddress)
let persistentStore = privatePersistentStore //share.persistentStore!
persistentContainer.fetchParticipants(matching: [lookupInfo], into: persistentStore) { (results, error) in
guard let participants = results, let participant = participants.first, error == nil else {
completionHandler?(share, error)
return
}
participant.permission = permission
participant.role = .privateUser
share.addParticipant(participant)
self.persistentContainer.persistUpdatedShare(share, in: persistentStore) { (share, error) in
if let error = error {
print("\(#function): Failed to persist updated share: \(error)")
}
completionHandler?(share, error)
}
}
}
func deleteParticipant(_ participants: [CKShare.Participant], share: CKShare,
completionHandler: ((_ share: CKShare?, _ error: Error?) -> Void)?) {
for participant in participants {
share.removeParticipant(participant)
}
/**
Use privatePersistentStore directly because only the owner may delete participants to a share.
*/
persistentContainer.persistUpdatedShare(share, in: privatePersistentStore) { (share, error) in
if let error = error {
print("\(#function): Failed to persist updated share: \(error)")
}
completionHandler?(share, error)
}
}
}
// Core Data models
extension TripsLog {
#NSManaged var name: String
#NSManaged var identifier: String
#NSManaged var entries: NSSet
}
extension Trip {
#NSManaged var identifier: String
#NSManaged var name: String
#NSManaged var date: Date
#NSManaged var comments: String?
#NSManaged var leaderName: String
#NSManaged var images: NSSet?
}
If anyone is able to shed any light on this I'd really appreciate it, as Apple's own documentation is somewhat lacking. Many thanks!

CoreData: This NSPersistentStoreCoordinator has no persistent stores. It cannot perform a save operation

I am facing this issue once or twice a day for the last week when I open my app & the app tries any save operation on the context, I still can't find a way to reproduce it.
I have searched many question on SO for fix, but most of them point 2 issues
Core Data Migration issue(which I don't have as I am on the same Model version no.)
failure of loading the persistent store (which is also doesn't happen in my case as my Core Data Stack doesn't initialise the main UI if the loadPersistentStores method on the persistentContainer fails)
I am using the Core Data stack setup mentioned in the below link:
https://williamboles.me/progressive-core-data-migration/
Here is my CoreData Setup class:
lazy var persistentContainer: NSPersistentContainer = {
let persistentContainer = NSPersistentContainer(name: "ABC")
let description = persistentContainer.persistentStoreDescriptions.first
description?.shouldInferMappingModelAutomatically = false //inferred mapping will be handled else where
description?.shouldMigrateStoreAutomatically = false
description?.type = storeType
return persistentContainer
}()
lazy var managedObjectContext: NSManagedObjectContext = {
let context = self.persistentContainer.newBackgroundContext()
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
context.automaticallyMergesChangesFromParent = true
return context
}()
lazy var _managedObjectContext: NSManagedObjectContext = {
let context = self.persistentContainer.viewContext
context.automaticallyMergesChangesFromParent = true
return context
}()
// MARK: - Singleton
private static var privateShared : CoreDataManager?
class func shared() -> CoreDataManager { // change class to final to prevent override
guard let uwShared = privateShared else {
privateShared = CoreDataManager()
return privateShared!
}
return uwShared
}
class func destroy() {
privateShared = nil
}
// MARK: - Init
init(storeType: String = NSSQLiteStoreType, migrator: CoreDataMigratorProtocol = CoreDataMigrator()) {
self.storeType = storeType
self.migrator = migrator
}
// MARK: - SetUp
func setup(completion: #escaping () -> Void) {
loadPersistentStore {
completion()
}
}
// MARK: - Loading
private func loadPersistentStore(completion: #escaping () -> Void) {
migrateStoreIfNeeded {
self.persistentContainer.loadPersistentStores { description, error in
guard error == nil else {
fatalError("was unable to load store \(error!)")
}
completion()
}
}
}
private func migrateStoreIfNeeded(completion: #escaping () -> Void) {
guard let storeURL = persistentContainer.persistentStoreDescriptions.first?.url else {
fatalError("persistentContainer was not set up properly")
}
if migrator.requiresMigration(at: storeURL, toVersion: CoreDataMigrationVersion.current) {
DispatchQueue.global(qos: .userInitiated).async {
self.migrator.migrateStore(at: storeURL, toVersion: CoreDataMigrationVersion.current)
DispatchQueue.main.async {
completion()
}
}
} else {
completion()
}
}
And I initialise the Core Data stack using the following code in the App Delegate:
CoreDataManager.shared().setup {[unowned self] in
self.showMainUI()
}
My App crashes after the Home Controller is loaded & some part of my code does a save operation on certain NSManagedObject Model
This how I save to Context:
let context = CoreDataManager.shared().managedObjectContext // background context
context.performAndWait {
if let entityDescription = NSEntityDescription.entity(forEntityName: Entity_Name, in: context) {
if let runEntityObject = NSManagedObject(entity: entityDescription, insertInto: context) as? MY_Model {
// Create the Object
guard context.hasChanges else { return }
do {
try context.save() // Crashes here once or twice a day :(
}
catch {
print(error.localizedDescription)
}
}
}
}
Some SO answers also mention of threading issues but I am using the performAndWait Block so the save happen on the same queue
Would be really helpful If someone pointed me in the right direction regarding this issue
After going through my AppDelegate file many times, I found that I was doing a Core Data save operation in the applicationDidBecomeActive method which is also called when the app starts from a suspended state.
So if my Core Data stack setup closure didn't finish before the applicationDidBecomeActive is called the app would crash.
After removing it, the app was working fine without any crashes

iOS Core Data - How to avoid crash when many context save at same time?

I try to avoid crash when many contexts save at same time.
The following class has one operation queue that operate only one work at same time. It has three context. First, defaultContext is main queue type, this is not directly updated and is only visible to the user. Other two contexts is localContext and externalContext.
LocalContext is for user's schedule addition and external Context is for external schedule update like cloud sync. Local context and external context is child of defaultContext and set it's automaticallyMergesChangesFromParent property to true. Even if user update and external update are implemented at same time. Since they run sequentially in the same queue, there is no data loss.
It works great when data input is small. But app gets slower when too much data is coming in. Is there any better way?
Here's my code.
class DataController {
static let shared = DataController()
var schedules: [Schedule] = []
var persistentContainer: NSPersistentContainer
let persistentContainerQueue = OperationQueue()
private init() {
persistentContainerQueue.maxConcurrentOperationCount = 1
persistentContainer = NSPersistentContainer(name: "CoreDataConcurrency")
persistentContainer.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Failed to load Core Data stack: \(error)")
}
}
}
lazy var defaultContext: NSManagedObjectContext = {
[unowned self] in
self.persistentContainer.viewContext
}()
lazy var localContext: NSManagedObjectContext = {
[unowned self] in
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.parent = self.defaultContext
context.automaticallyMergesChangesFromParent = true
return context
}()
lazy var externalContext: NSManagedObjectContext = {
[unowned self] in
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.parent = self.defaultContext
context.automaticallyMergesChangesFromParent = true
return context
}()
func enqueueCoreDataOperation(context: NSManagedObjectContext, changeBlock: #escaping () -> (NSManagedObjectContext)) {
persistentContainerQueue.addOperation {
let changedContext = changeBlock()
guard changedContext.hasChanges else {
return
}
changedContext.performAndWait({
do {
try changedContext.save()
if let parentContext = changedContext.parent {
do {
try parentContext.save()
} catch {
fatalError()
}
}
} catch {
fatalError()
}
})
}
}
func addSchedule(title: String, date: Date, context: NSManagedObjectContext) {
let changeBlock: () -> (NSManagedObjectContext) = {
let schedule = NSEntityDescription.insertNewObject(forEntityName: "Schedule", into: context) as! Schedule
schedule.title = title
schedule.date = date
return context
}
enqueueCoreDataOperation(context: context, changeBlock: changeBlock)
}
func updateSchedule(schedule: Schedule, modifiedTitle: String, context: NSManagedObjectContext) {
let scheduleInContext = context.object(with: schedule.objectID) as! Schedule
let changeBlock: () -> (NSManagedObjectContext) = {
scheduleInContext.title = modifiedTitle
return context
}
enqueueCoreDataOperation(context: context, changeBlock: changeBlock)
}
}
You could batch up the incoming data into smaller batches so each operation takes less time and add a priority to the operations so cloud based changes have a lower priority. Then they will no longer block the other changes. But I strongly suspect that you are doing something wrong in you import operation that is making take too long. Are you doing a fetch for each imported entity? Please share that code.

Unit testing DI with CoreData won't work

I'm trying to write a test to make sure that my view model's model property when set calls my fetchPlan method from the model and then sets my 'plan' property in my view model. It seems to be setting the property but the values are missing...
Here's my view model
final class PlanProgressViewModel: PlanProgressViewModelView {
// MARK: - Properties
fileprivate var plan: PlanData?
// MARK: - PlanProgressViewModelView
weak var viewDelegate: PlanProgressViewModelViewDelegate?
var model: PlanModel? {
didSet {
model?.fetchCurrentPlan(completionHandler: { (plan) in
guard let plan = plan else {return}
self.plan = plan
})
}
}
// Testing this fails...
var planName: String! {
guard let plan = plan else {return "No plan"}
return plan.name
}
var planProgressionString: String! {
return "\(Int(round(self.progress * 100)))%"
}
var progress: Double! {
guard let plan = plan, let workouts = plan.workouts, let completedWorkouts = plan.completedWorkouts else {return 0}
return Double(Int(completedWorkouts) / workouts.count)
}
}
Here's my test suite, i'm using a mock to return hardcoded data from the model.
var sut: PlanProgressViewModel!
var model: MockPlanModel!
var moc: NSManagedObjectContext!
override func setUp() {
super.setUp()
moc = setupInMemoryMOC()
let mockModel = MockPlanModel(moc: moc)
model = mockModel
let viewModel = PlanProgressViewModel()
viewModel.model = model
sut = viewModel
}
override func tearDown() {
moc = nil
model = nil
sut = nil
super.tearDown()
}
// This passes
func testModelFetchesCurrentPlanOnce() {
XCTAssertEqual(model.fetchPlanWasCalled, 1)
}
// This is failing
func testPlanName() {
XCTAssertEqual(sut.planName, "Test plan")
}
Here's my method for setting up a in memory persistent store...
public func setupInMemoryMOC() -> NSManagedObjectContext {
let mom = NSManagedObjectModel.mergedModel(from: [Bundle.main])
let psc = NSPersistentStoreCoordinator(managedObjectModel: mom!)
do {
try psc.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil)
} catch {
fatalError()
}
let moc = NSManagedObjectContext.init(concurrencyType: .mainQueueConcurrencyType)
moc.persistentStoreCoordinator = psc
return moc }
Here's my mock for the model which returns hardcoded data...
public class MockPlanModel: MWPlanModel {
var fetchPlanWasCalled = 0
override public func fetchCurrentPlan(completionHandler: #escaping (_ plan: PlanData?) -> ()) {
fetchPlanWasCalled += 1
let moc = setupInMemoryMOC()
let plan = createTestPlan(moc: moc)
completionHandler(plan)
}}
Here's my helper method for creating the model object, (PlanData is a protocol that my 'Plan' NSManaged object inherits).
public func createTestPlan(moc: NSManagedObjectContext) -> PlanData {
let plan: Plan = Plan(context: moc)
plan.name = "Test plan"
plan.completedWorkouts = 5
plan.currentPlan = true
for _ in 0..<5 {
plan.mutableOrderedSetValue(forKeyPath: #keyPath(Plan.workouts)).add(createTestCompletedWorkout(moc: moc))
}
return plan }
The plan name should be "Test plan" as that is what i set the hardcoded value to be but it fails and returns an empty string instead...
Really stuck on this, i'm fairly new to testing so i appreciate any help with this. Thanks
#MartinR comment good point out :
The non-optional on the left side "Test plan" gets automatically promoted to an optional. because sut.planName is an optional. more check this Swift comparing Strings optionals vs non-optional
You need to unwrap sut.planName
if let planName = sut.planName{
XCTAssertEqual(planName, "Test plan")
}
Or try this way :
XCTAssertEqual((sut.planName ?? ""), "Test plan")
Note: If you face still same issue then update your Xcode
// Using Apple XCTest (Xcode 7.3.1), this produces the output:
// "XCTAssertEqual failed: ("Optional(1)") is not equal to ("Optional(2)") - "
XCTAssertEqual(1, 2)
More details : Here Bug reported

the interface is not updated with the latest data saved to core data swift

these days I start to learn ios applications development using SWIFT language
so I started to build my own app which contain forms that collect information from users and save\retrieve it to\from core data
my home page hide/show its buttons depending on data retrieved from the core data and do simple check on it so the data have to be up to date to avoid mistakes
but when I add user to the Core data and return to the home page it show the buttons as nothing has been added but if leave the home page to other page and then back to home page then the last user added appears
it seems like the context did not finish the data saving before the home appears
How I can fix that and ensure that the context object finish saving then show the home page
thanks a lot
Please keep in mind that waiting for context to save before performing segue might be not the best solution - depending on task it can take a long time. If use this approach you should show some progress indicator to user or smth.
Otherwise it will look like your app UI is freezing and that is a bad UX.
Anyway answering your question you have 3 basic solutions :
use competition closure
use delegation
use notifications
I assume you use some sort of custom class to load the CoreData Stack and you probably have function for saving context. Than it might look like this :
private func saveContext(completition : (()->() )?) {
if let moc = self.context {
var error : NSError? = nil
if moc.hasChanges && !moc.save(&error){
println(error?.localizedDescription)
abort()
}
//Call delegate method
delegate?.MiniCoreDataStackDidSaveContext()
//Send notification message
defaultCenter.postNotificationName("MyContextDidSaveNotification", object: self)
//Perform completition closure
if let closure = completition {
closure()
}
}
}
And you use it like this :
MyCoreDataStack.saveContext(){
performSegueWithIdentifier(SEGUE_ID,nil)
}
or
NSNotificationCenter.defaultCenter().addObserverForName("MyContextDidSaveNotification",
object: MyCoreDataStack.saveContext,
queue: NSOperationQueue.mainQueue(),
usingBlock: { _ in performSegueWithIdentifier(SEGUE_ID, sender: nil) }
)
In case you don't have any Stack - I've written this small singleton class as an example it lacks of proper error handling etc.
In a private function saveContext it combines all three approaches (it's only for example, I would not advice to use delegation with singleton pattern)
import CoreData
protocol MiniCoreDataStackDelegate : class {
func MiniCoreDataStackDidSaveContext()
}
#objc(MiniCoreDataStack)
class MiniCoreDataStack {
struct Constants {
static let persistentStoreName = "Store"
static let contextSaveNotification = "MiniCoreDataStackDidSaveContextNotification"
}
private var managedObjectModel : NSManagedObjectModel
private var persistentStoreCoordinator : NSPersistentStoreCoordinator? = nil
private var store : NSPersistentStore?
private let defaultCenter = NSNotificationCenter.defaultCenter()
var defaultContext : NSManagedObjectContext!
var stackIsLoaded : Bool = false
weak var delegate : MiniCoreDataStackDelegate?
class var defaultModel: NSManagedObjectModel {
return NSManagedObjectModel.mergedModelFromBundles(nil)!
}
class var sharedInstance: MiniCoreDataStack {
struct Singleton {
static let instance = MiniCoreDataStack()
}
return Singleton.instance
}
class func storesDirectory() -> NSURL {
let applicationDocumentsDirectory = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory,inDomains: .UserDomainMask).last as! NSURL
return applicationDocumentsDirectory
}
private func storeURLForName(name:String) -> NSURL {
return MiniCoreDataStack.storesDirectory().URLByAppendingPathComponent("\(name).sqlite")
}
func localStoreOptions() -> NSDictionary {
return [
NSInferMappingModelAutomaticallyOption:true,
NSMigratePersistentStoresAutomaticallyOption:true
]
}
init( model : NSManagedObjectModel = MiniCoreDataStack.defaultModel){
managedObjectModel = model
}
func openStore(completion:(()->Void)?) {
println("\(NSStringFromClass(self.dynamicType)): \(__FUNCTION__)")
var error: NSError? = nil
let tempPersistenStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
if let newStore = tempPersistenStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: self.storeURLForName(Constants.persistentStoreName), options: self.localStoreOptions() as [NSObject : AnyObject], error: &error){
self.persistentStoreCoordinator = tempPersistenStoreCoordinator
defaultContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
defaultContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
defaultContext.persistentStoreCoordinator = persistentStoreCoordinator
self.stackIsLoaded = true
println("\(NSStringFromClass(self.dynamicType)): Store loaded")
if let completionClosure = completion {
completionClosure()
}
} else {
println("\(NSStringFromClass(self.dynamicType)): !!! Could not add persistent store !!!")
println(error?.localizedDescription)
}
}
private func saveContext(context: NSManagedObjectContext? = MiniCoreDataStack.sharedInstance.defaultContext!, completition : (()->() )?) {
if !self.stackIsLoaded {
return
}
if let moc = context {
var error : NSError? = nil
if moc.hasChanges && !moc.save(&error){
println(error?.localizedDescription)
abort()
}
//Call delegate method
delegate?.MiniCoreDataStackDidSaveContext()
//Send notification message
defaultCenter.postNotificationName(Constants.contextSaveNotification, object: self)
//Perform completition closure
if let closure = completition {
closure()
}
}
}
func save(context: NSManagedObjectContext? = MiniCoreDataStack.sharedInstance.defaultContext!,completition : (()->() )? ) {
//Perform save on main thread
if (NSThread.isMainThread()) {
saveContext(context: context,completition: completition)
}else {
NSOperationQueue.mainQueue().addOperationWithBlock(){
self.saveContext(context: context, completition : completition)
}
}
}
func fetchResultsControllerForEntity(entity : NSEntityDescription, predicate :NSPredicate? = nil, sortDescriptors:[NSSortDescriptor]? = nil, sectionNameKeyPath:String? = nil, cacheName: String? = nil,inManagedContext context : NSManagedObjectContext? = nil ) ->NSFetchedResultsController {
let fetchRequest = NSFetchRequest()
fetchRequest.entity = entity
fetchRequest.sortDescriptors = sortDescriptors
fetchRequest.predicate = predicate
fetchRequest.fetchBatchSize = 25
var aContext = context ?? self.defaultContext!
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: aContext, sectionNameKeyPath: sectionNameKeyPath, cacheName: cacheName)
var error: NSError?
if !fetchedResultsController.performFetch(&error){
println("Could not fetch : \(error)")
}
return fetchedResultsController
}
func executeFetchRequest(request : NSFetchRequest, context: NSManagedObjectContext? = nil) -> [NSManagedObject] {
var fetchedObjects = [NSManagedObject]()
let managedContext = context ?? defaultContext
managedContext?.performBlockAndWait{
var error: NSError?
if let result = managedContext?.executeFetchRequest(request, error: &error) {
if let managedObjects = result as? [NSManagedObject] {
fetchedObjects = managedObjects
}
}
if let err = error{
println(err)
}
}
return fetchedObjects
}
func insertEntityWithClassName(className :String, andAttributes attributesDictionary : NSDictionary? = nil, andContext context : NSManagedObjectContext = MiniCoreDataStack.sharedInstance.defaultContext ) -> NSManagedObject {
let entity = NSEntityDescription.insertNewObjectForEntityForName(className, inManagedObjectContext: context) as! NSManagedObject
if let attributes = attributesDictionary {
attributes.enumerateKeysAndObjectsUsingBlock({
(dictKey : AnyObject!, dictObj : AnyObject!, stopBool) -> Void in
entity.setValue(dictObj, forKey: dictKey as! String)
})
}
return entity
}
func deleteEntity(entity: NSManagedObject){
self.defaultContext!.deleteObject(entity)
}
}
Using Stack :
//Open store
MiniCoreDataStack.sharedInstance.openStore()
//Insert Entity
let newEntity = MiniCoreDataStack.sharedInstance.insertEntityWithClassName(YourEntityName)
//Saving
MiniCoreDataStack.sharedInstance.save(){
// completition closure
}
//Perform fetch request
MiniCoreDataStack.sharedInstance.executeFetchRequest(YourFetchRequest)

Resources