CloudKit and NSFetchedResultsController - swift

I have a very simple app that consists of a ListObject, each ListObject can have ItemObjects.
I wanted to use CloudKit as my backend. I have the following code.
override public init() {
super.init()
cloudContainer = NSPersistentCloudKitContainer(name: "ListModel")
cloudContainer.loadPersistentStores { storeDescription, error in
if let error = error {
print("Unresolved error \(error)")
return
}
self.viewContext = self.cloudContainer.viewContext
self.viewContext.automaticallyMergesChangesFromParent = true
self.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
do {
try self.fetchedResultsController.performFetch()
} catch {
let fetchError = error as NSError
print("Unable to Perform Fetch Request")
print("\(fetchError), \(fetchError.localizedDescription)")
}
}
}
lazy var fetchedResultsController: NSFetchedResultsController<ListObject> = {
let fetchRequest: NSFetchRequest<ListObject> = ListObject.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "dateCreated",
ascending: false)]
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: self.viewContext,
sectionNameKeyPath: nil,
cacheName: nil)
fetchedResultsController.delegate = self
return fetchedResultsController
}()
var lists:[ListObject] {
get {
guard let list = fetchedResultsController.fetchedObjects else {
return [ListObject]()
}
return list
}
set {}
}
I listen for updates to the FetchedController and update the table view accordingly:
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
print("Update received")
self.delegate?.reloadTable()
}
However the controllerDidChangeContent function seems to be called randomly, and only sometimes (e.g. on loading the app). Is there another notification I should be listening for when the CloudKit db is modified (e.g. on the macOS client)

Related

What is the safest way to create the coredata stack

In order to initiate the coredata stack in app delegate is not recommended as it will create issue in a multi threaded environment. Therefore, I created a separate class for coredata stack and then a separate class to handle operations. And in the Coredata stack class it crashes. What am I missing here ?
CoreData Stack Class
Code looks like this
class CoreDataManager {
static let sharedManager = CoreDataManager()
private init() {} //Singleton
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "MyDatabase")
container.loadPersistentStores(completionHandler: { (_, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
lazy var viewContext: NSManagedObjectContext = {
return self.persistentContainer.viewContext
}()
lazy var cacheContext: NSManagedObjectContext = {
return self.persistentContainer.newBackgroundContext()
}()
lazy var updateContext: NSManagedObjectContext = {
let _updateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
_updateContext.parent = self.viewContext
return _updateContext
}()
}
This is how I access it
class StorageManager: NSObject {
lazy var managedObjectContext: NSManagedObjectContext = {
return CoreDataManager.sharedManager.persistentContainer.viewContext
}()
lazy var privateMOC: NSManagedObjectContext = {
return CoreDataManager.sharedManager.updateContext
}()
private func synchronize(privateMOC: NSManagedObjectContext) {
do {
try privateMOC.save()
self.managedObjectContext.performAndWait {
do {
try self.managedObjectContext.save()
// "Saved to main context"
} catch {
print("Could not synchonize data. \(error), \(error.localizedDescription)")
}
}
} catch {
print("Could not synchonize data. \(error), \(error.localizedDescription)")
}
}
}
A sample operation would look like this
func deleteContact(contactID: String) {
let privateMOC = self.privateMOC
privateMOC.performAndWait {
let request: NSFetchRequest<Contact> = Contact.fetchRequest()
request.predicate = NSPredicate(format: "contact_id= %#", contactID)
let result = try? privateMOC.fetch(request)
for object in result! {
privateMOC.delete(object)
}
self.synchronize(privateMOC: privateMOC)
}
}

UserDefaults for NSMutableAttributedString

I have a textView which is the whole screen of the app. The users input new text, change the properties etc. I want the app save textview content with its properties for next times. I came up with userDefaults. I tried much but nothing happens. I couldn't find a solution to this. I tried this solution from stackoverflow https://stackoverflow.com/a/36940864/12698480 but no change. Again empty screen comes on the textview. My code as an extension to Userdefaults is as below:
//Save text
func setNSMutableText(string: NSMutableAttributedString?, forKey key: String){
var stringData: NSData?
if let string = string {
do {
stringData = try NSKeyedArchiver.archivedData(withRootObject: string, requiringSecureCoding: false) as NSData?
set(stringData, forKey: key)
print("archived NSMutableData")
} catch let err {
print("error archiving string data", err)
}
}
}
//LOAD text
func getNSMutableForKey(key: String) -> NSMutableAttributedString? {
var string: NSMutableAttributedString?
if let stringData = data(forKey: "screentextKey") {
do {
string = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSMutableAttributedString.self, from: stringData)
print("UNARchived string data!!!")
} catch let err {
print("error extracting string data", err)
}
}
return string
}
Usage:
//For saving
UserDefaults.standard.setNSMutableText(string: NSMutableAttributedString(attributedString: myTextView.attributedText), forKey: "screentextKey")
//For loading
override func viewDidLoad() {
super.viewDidLoad()
let oldstring = (UserDefaults.standard.getNSMutableForKey(key: "screentextKey"))!
myTextView.attributedText = oldstring
}
What is the problem? I saw that someone's suggestion about using NSAttributedString at the place of NSMutableAttributedString. I tried that way also, but no change.
In order to make it work the way you want, you don't have to use NSKeyedArchiver.archivedData. Instead it is much better to use CoreData to save the data in device memory(Because if I got the idea it is not the big amount of data), Structure it using XCDataModel and then, fetch it using NSFetchController.
in XCDataModel you have to create ENTITY and set attributes for it(String, Bool, Double etc.)
After that you will have to set NSFetchedResultsController in order to manage the process.
Here is the way I would set it :
Import CoreData
set persistent container(Container for your data):
static let persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "RebelFitness")
container.loadPersistentStores { (_, error) in
if let error = error as NSError? {
fatalError("Unresolved error (error), (error.userInfo)")
}
}
return container
}()
set NSFetchedResultsController:
lazy var fetchedResultsController: NSFetchedResultsController = {
let fetchRequest: NSFetchRequest< YourEntityName > = YourEntityName.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "distance", ascending: true), NSSortDescriptor(key: "duration", ascending: true)] //Here you use descriptor to sort the data in my example distance comes in array first and then any other elements
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: CoreDataStack.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
fetchedResultsController.delegate = self as? NSFetchedResultsControllerDelegate
return fetchedResultsController
}()
Function used to fetch the saved data:
static var context: NSManagedObjectContext { return persistentContainer.viewContext }
In this function we are fetching data to new array which will pass it to your array which supplies the UIKit elements with data
func loadIsCurrent() -> [YourEntityName] {
var _data : [YourEntityName] = []
let context = persistentContainer.viewContext
let fetchRequest: NSFetchRequest< YourEntityName > = YourEntityName.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "distance", ascending: false), NSSortDescriptor(key: "duration", ascending: true)]
do {
let data = try context.fetch(fetchRequest)
if(sessions.count > 0) {
_data = data
}
} catch {
print("Something happened while trying to retrieve tasks...")
}
return _data
}
saving the data to coreData:
func saveContext () {
let context = persistentContainer.viewContext
guard context.hasChanges else {
return
}
do {
try context.save()
try fetchedResultsController.performFetch()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}

Refreshing Core Data after changes from main app or extension

I have been working on a today widget for my Core Data app. I use a NSFetchedResultsController for the widget and main app and am notified when the data changed/added from the opposite target using UserDefaults and help from this post.
But updating the data or the NSFetchedResultsController to see the changes/additions does not work. I have tried:
refetching the data from the NSFetchedResultsController
setting the persistent container's view context stalenessInterval to 0 and calling viewContext.refreshAllObjects() then try to refetch the data (and without)
setting shouldRefreshRefetchedObjects to true on the fetchedController so it will automatically call.
I know the data is being saved because if I force quit the app or re-run the widget the new data is there. But I can not figure out how to refresh the app or the widget when the opposite has changed something.
I have been looking for a solution for the past couple of days and this is literally the last thing I need to be done with this widget. Please if anyone knows how to do this please help!
How I set up my NSFetchedResultsController:
lazy var fetchedResultsController: NSFetchedResultsController<Item> = setupFetchedController()
override func viewDidAppear(_ animated: Bool) {
loadData()
//check to see if any data was changed. Being notified about changes works every time
subscribeForChangesObservation()
}
private func setupFetchedController() -> NSFetchedResultsController<Item> {
let managedContext = CoreDataManager.sharedManager.persistentContainer.viewContext
let sortDescriptor = NSSortDescriptor(key: "date", ascending: true)
let request : NSFetchRequest<Item> = Item.fetchRequest()
request.predicate = NSPredicate(format: "date <= %#", Date() as NSDate)
request.sortDescriptors = [sortDescriptor]
fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: managedContext, sectionNameKeyPath: nil, cacheName: nil)
fetchedResultsController.delegate = self
return fetchedResultsController
}
private func loadData() {
do {
try fetchedResultsController.performFetch()
updateSnapshot()
} catch {
print("Hey Listen! Error performing fetchedResultsController fetch: \(error)")
}
}
//reloads the items in the table
func updateSnapshot() {
let fetchedItems = fetchedResultsController.fetchedObjects ?? []
var snapshot = NSDiffableDataSourceSnapshot<Int, Item>()
snapshot.appendSections([0])
snapshot.appendItems(fetchedItems)
dataSource.apply(snapshot, animatingDifferences: true)
}
I use diffable data sources (came out with iOS 13) with the NSFetchedResultsController, but I don't this doesn't have anything to do with the problem, because I tried without it and the same issue happens.
How I set up Core Data:
class CoreDataManager {
static let sharedManager = CoreDataManager()
private init() {}
lazy var persistentContainer: NSPersistentContainer = {
var useCloudSync = UserDefaults.standard.bool(forKey: "useCloudSync")
//Get the correct container
let containerToUse: NSPersistentContainer?
if useCloudSync {
//custom container to just set the defaultDirectoryURL to the app group url
containerToUse = GroupedPersistentCloudKitContainer(name: "App")
} else {
containerToUse = NSPersistentContainer(name: "App")
}
guard let container = containerToUse else {
fatalError("Couldn't get a container")
}
//Set the storeDescription
let storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.App")!.appendingPathComponent("\(container.name).sqlite")
var defaultURL: URL?
if let storeDescription = container.persistentStoreDescriptions.first, let url = storeDescription.url {
defaultURL = FileManager.default.fileExists(atPath: url.path) ? url : nil
}
if defaultURL == nil {
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: storeURL)]
}
let description = container.persistentStoreDescriptions.first else {
fatalError("Hey Listen! ###\(#function): Failed to retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
if !useCloudSync {
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
//migrate from old url to use app groups
if let url = defaultURL, url.absoluteString != storeURL.absoluteString {
let coordinator = container.persistentStoreCoordinator
if let oldStore = coordinator.persistentStore(for: url) {
do {
try coordinator.migratePersistentStore(oldStore, to: storeURL, options: nil, withType: NSSQLiteStoreType)
} catch {
print("Hey Listen! Error migrating persistent store")
print(error.localizedDescription)
}
// delete old store
let fileCoordinator = NSFileCoordinator(filePresenter: nil)
fileCoordinator.coordinate(writingItemAt: url, options: .forDeleting, error: nil, byAccessor: { url in
do {
try FileManager.default.removeItem(at: url)
} catch {
print("Hey Listen! Error deleting old persistent store")
print(error.localizedDescription)
}
})
}
}
}
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = appTransactionAuthorName
// Pin the viewContext to the current generation token and set it to keep itself up to date with local changes.
container.viewContext.automaticallyMergesChangesFromParent = true
do {
try container.viewContext.setQueryGenerationFrom(.current)
} catch {
fatalError("Hey Listen! ###\(#function): Failed to pin viewContext to the current generation:\(error)")
}
// Observe Core Data remote change notifications.
NotificationCenter.default.addObserver(
self, selector: #selector(type(of: self).storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)
return container
}
}
Have you included this in your viewContext setup?
persistentContainer.viewContext.automaticallyMergesChangesFromParent = true

Update tableview after NSBatchDeleteRequest with animation

I was wondering is there a way to update a table view with an animation, when a NSBatchRequest is executed? This code does the job, but there is no animation it just removes everything really quick.
#objc func resetButtonPressed() {
let deleteRequest = NSBatchDeleteRequest(fetchRequest: Company.fetchRequest())
let context = CoreDataManager.shared.persistentContainer.viewContext
do {
try context.execute(deleteRequest)
try context.save()
try companyController.performFetch()
tableView.reloadData()
} catch let err {
print("Unable to perform company batch delete request", err)
}
}
I am trying to get a similar animation when you use an array. The code looks something like this.
#objc func resetButtonPressed() {
// This Basically does the batch delete request
CoreDataManager.shared.deleteAllCompanies()
var indexsToRemove = [IndexPath]()
for (index, _) in companies.enumerated() {
let indexPath = IndexPath(row: index, section: 0)
indexsToRemove.append(indexPath)
}
companies.removeAll()
tableView.deleteRows(at: indexsToRemove, with: .left)
}
I tried doing this but no luck. My app just keep crashing.
#objc func resetButtonPressed() {
let deleteRequest = NSBatchDeleteRequest(fetchRequest: Company.fetchRequest())
let context = CoreDataManager.shared.persistentContainer.viewContext
do {
var indexsToRemove = [IndexPath]()
for (_,company) in companyController.fetchedObjects!.enumerated() {
guard let indexPath = companyController.indexPath(forObject: company) else { return }
indexsToRemove.append(indexPath)
}
try context.execute(deleteRequest)
try context.save()
//Doesn't allow me to use this because its only a get
companyController.fetchedObjects?.removeAll()
//This keeps crashing my app.
tableView.deleteRows(at: indexsToRemove, with: .fade)
} catch let err {
print("Unable to perform company batch delete request", err)
}
}
Also my companyController have multiple sections. It look something like this.
lazy var companyController: NSFetchedResultsController<Company> = {
let context = CoreDataManager.shared.persistentContainer.viewContext
let request: NSFetchRequest<Company> = Company.fetchRequest()
let nameSort = NSSortDescriptor(key: "name", ascending: true)
let dateSort = NSSortDescriptor(key: "date", ascending: true)
request.sortDescriptors = [nameSort,dateSort]
//Company has a type property ex: Electronics, Gas, Groceries ...
let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: "type", cacheName: nil)
controller.delegate = self
do {
try controller.performFetch()
} catch let err {
print("Unable to fetch company in controller", err)
}
return controller
}()

Swift Core Data Class method?

I'm currently learning Core Data and I have two view controllers that are using the same piece of code to get a users profile. The problem is that it's the same code copy and pasted and I would like to avoid this. I'm using the Managed Class approach to access the data and each controller has the following method:
var profileHolder: Profile!
let profileRequest = Profile.createFetchRequest()
profileRequest.predicate = NSPredicate(format: "id == %d", 1)
profileRequest.fetchLimit = 1
if let profiles = try? context.fetch(profileRequest) {
if profiles.count > 0 {
profileHolder = profiles[0]
}
}
if profileHolder == nil {
let newProfile = Profile(context: context)
newProfile.id = 1
newProfile.attempts = nil
profileHolder = newProfile
}
profile = profileHolder
Profile is a var inside the controller: var profile: Profile! and I call the above inside viewWillAppear()
I know there's a cleaner approach and I would like to move this logic inside the class but unsure how to.
Thanks
var profileHolder: Profile!
profileHolder here is force unwrapping optional value. And you are fetching from core data and assigning the value in viewWillAppear, which is risky as profileHolder would be nil and can trigger crash if you access it before viewWillAppear.
My suggestion would be:
var profileHolder: Profile
{
if let profiles = try? context.fetch(profileRequest),
profiles.count > 0
{
return profiles[0]
}
else
{
let newProfile = Profile(context: context)
newProfile.id = 1
newProfile.attempts = nil
return newProfile
}
}()
This will ensure profileHolder is either fetched or created when the view controller is initialised.
However this would not work if
context
is a stored property of viewController, in which case, do:
var profileHolder: Profile?
override func viewDidLoad()
{
if let profiles = try? context.fetch(profileRequest),
profiles.count > 0
{
return profiles[0]
}
else
{
let newProfile = Profile(context: context)
newProfile.id = 1
newProfile.attempts = nil
return newProfile
}
}
Here is the struct I created for a project I did that allows me to access my CoreData functions anywhere. Create a new empty swift file and do something like this.
import CoreData
// MARK: - CoreDataStack
struct CoreDataStack {
// MARK: Properties
private let model: NSManagedObjectModel
internal let coordinator: NSPersistentStoreCoordinator
private let modelURL: URL
internal let dbURL: URL
let context: NSManagedObjectContext
let privateContext: NSManagedObjectContext
// MARK: Initializers
init?(modelName: String) {
// Assumes the model is in the main bundle
guard let modelURL = Bundle.main.url(forResource: modelName, withExtension: "momd") else {
print("Unable to find \(modelName)in the main bundle")
return nil
}
self.modelURL = modelURL
// Try to create the model from the URL
guard let model = NSManagedObjectModel(contentsOf: modelURL) else {
print("unable to create a model from \(modelURL)")
return nil
}
self.model = model
// Create the store coordinator
coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
// create a context and add connect it to the coordinator
//context.persistentStoreCoordinator = coordinator
privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.persistentStoreCoordinator = coordinator
context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context.parent = privateContext
// Add a SQLite store located in the documents folder
let fm = FileManager.default
guard let docUrl = fm.urls(for: .documentDirectory, in: .userDomainMask).first else {
print("Unable to reach the documents folder")
return nil
}
self.dbURL = docUrl.appendingPathComponent("model.sqlite")
// Options for migration
let options = [NSInferMappingModelAutomaticallyOption: true,NSMigratePersistentStoresAutomaticallyOption: true]
do {
try addStoreCoordinator(NSSQLiteStoreType, configuration: nil, storeURL: dbURL, options: options as [NSObject : AnyObject]?)
} catch {
print("unable to add store at \(dbURL)")
}
}
// MARK: Utils
func addStoreCoordinator(_ storeType: String, configuration: String?, storeURL: URL, options : [NSObject:AnyObject]?) throws {
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: dbURL, options: nil)
}
}
// MARK: - CoreDataStack (Removing Data)
internal extension CoreDataStack {
func dropAllData() throws {
// delete all the objects in the db. This won't delete the files, it will
// just leave empty tables.
try coordinator.destroyPersistentStore(at: dbURL, ofType:NSSQLiteStoreType , options: nil)
try addStoreCoordinator(NSSQLiteStoreType, configuration: nil, storeURL: dbURL, options: nil)
}
}
// MARK: - CoreDataStack (Save Data)
extension CoreDataStack {
func saveContext() throws {
/*if context.hasChanges {
try context.save()
}*/
if privateContext.hasChanges {
try privateContext.save()
}
}
func autoSave(_ delayInSeconds : Int) {
if delayInSeconds > 0 {
do {
try saveContext()
print("Autosaving")
} catch {
print("Error while autosaving")
}
let delayInNanoSeconds = UInt64(delayInSeconds) * NSEC_PER_SEC
let time = DispatchTime.now() + Double(Int64(delayInNanoSeconds)) / Double(NSEC_PER_SEC)
DispatchQueue.main.asyncAfter(deadline: time) {
self.autoSave(delayInSeconds)
}
}
}
}
Create a class(CoreDataManager) that can manage core data operations.
import CoreData
class CoreDataManager:NSObject{
/// Application Document directory
lazy var applicationDocumentsDirectory: URL = {
let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return urls[urls.count-1]
}()
/// Core data manager
static var shared = CoreDataManager()
/// Managed Object Model
lazy var managedObjectModel: NSManagedObjectModel = {
let modelURL = Bundle.main.url(forResource: “your DB name”, withExtension: "momd")!
return NSManagedObjectModel(contentsOf: modelURL)!
}()
/// Persistent Store Coordinator
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.appendingPathComponent("SingleViewCoreData.sqlite")
var failureReason = "There was an error creating or loading the application's saved data."
let options = [ NSInferMappingModelAutomaticallyOption : true,
NSMigratePersistentStoresAutomaticallyOption : true]
do {
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: options)
persistanceStoreKeeper.sharedInstance.persistanceStorePath = url
} catch {
var dict = [String: AnyObject]()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" as AnyObject
dict[NSLocalizedFailureReasonErrorKey] = failureReason as AnyObject
dict[NSUnderlyingErrorKey] = error as NSError
let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
abort()
}
return coordinator
}()
/// Managed Object Context
lazy var managedObjectContext: NSManagedObjectContext = {
let coordinator = self.persistentStoreCoordinator
var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = coordinator
return managedObjectContext
}()
/// Save context
func saveContext () {
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch {
let nserror = error as NSError
NSLog("Unresolved error \(nserror), \(nserror.userInfo)")
abort()
}
}
}
}
Add the bellow function in your class.
func fetchProfile(profileId:String,fetchlimit:Int,completion: ((_ fetchedList:["Your model class"]) -> Void)){
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Your entity name")
let predicate:NSPredicate = NSPredicate(format: "id = %#", profileId)
fetchRequest.predicate=predicate
fetchRequest.fetchLimit = fetchlimit
do {
let results =
try CoreDataManager.shared.managedObjectContext.fetch(fetchRequest)
let profileList:["Your model class"] = results as! ["Your model class"]
if(profileList.count == 0){
//Empty fetch list
}
else{
completion(profileList)
}
}
catch{
//error
}
}
replace "Your model class" according to your requirement.
You can call the function "fetchProfile" and you will get the result inside the completion block.