Every time, when user launches application, app sends request to API and receive Data from it. I have to storage old data in a file on device and update them with every launch. When size of Data will be over than 3-5 mb, reading can be very long, isn't it?
My questions is: if i will store more than 5 mb of Data in file, can i quickly fetchs them, or only use database?
If yes, how can i create directory and put file to it?
And should i archive them?
I had a similar task and used CoreData with background fetch via DispatchQueue. I think this should help.
EDIT:
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
DispatchQueue.global(qos: .background).async {
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "ENTITY")
do {
self."NSManagedObject ARRAY" = try managedContext.fetch(fetchRequest)
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
DispatchQueue.main.async {
//MAKING CHANGES TO VIEW
}
}
P.S. I don't know how fast this fetch is, though it helped a lot in my task.
Related
Very rarely (<1% of users), a user will inform me that they open my app, and all their data is gone. The problem I'm having is that I am unable to replicate the issue because it happens so rarely. I don't even know how to begin fixing this problem. It seems like this does not happen while a user is using the app. They seem to just open it one day and the data is no longer there.
On one occasion, a user was able to recover the data by turning off and on the iCloud switch for my app (in the phone settings). However, this did not work for another user.
I was reading that iOS may delete the cache when the storage is full. However, to my understanding, the default location for the container is in Application Support.
Here is my Core Data implementation in App Delegate:
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentCloudKitContainer(name: "Bench")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
print("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
return container
}()
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
context.perform {
do {
try context.save()
} catch {
let nserror = error as NSError
print(nserror.localizedDescription)
}
}
}
}
I access it globally like this:
let ad = UIApplication.shared.delegate as! AppDelegate
let context = ad.persistentContainer.viewContext
I am updating a pre-iOS13 Core Data app to use Core Data+CloudKit syncing to support single users on multiple devices. The syncing is supposed to occur automagically, and in an interim step in my development it did work. Now it's not working, with CloudKit telemetry not registering any activity, and I can't figure out why it's not working.
In my previous app versions I provided a small number of label strings in UserDefaults and allowed users to modify them, with the updated versions put back into UserDefaults. It was a shortcut to avoid having to manage a second Core Data entity for what would only ever be a small number of objects.
I have since realized this won't work in a multi-device implementation because the fact that the local Core Data database is empty no longer means it's the first use for that user. Instead, each new device needs to look to the cloud-based data source to find the user's in-use label strings, and only use app-provided default values if the user doesn't already have something else.
I followed Apple's instructions for Setting Up Core Data with CloudKit, and at first syncing worked fine. But then I realized the syncing behavior wasn't correct, and that instead of pre-populating from strings stored in UserDefaults I really needed to provide a pre-populated Core Data database (.sqlite files). I implemented that and the app now works fine locally after copying the bundled .sqlite files at first local launch.
But for some reason this change caused the CloudKit syncing to stop working. Now, not only do I not get any automagical updates on other devices, I get no results in the CloudKit dashboard telemetry so it appears that CloudKit synching never gets started. This is odd because locally I am getting notifications of 'remote' changes that just occurred locally (the function I list as a #selector for the notification is being called locally when I save new data locally).
I'm stumped as to what the problem/solution is. Here is my relevant code.
//In my CoreDataHelper class
lazy var context = persistentContainer.viewContext
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let appName = Bundle.main.infoDictionary!["CFBundleName"] as! String
let container = NSPersistentCloudKitContainer(name: appName)
//Pre-load my default Core Data data (Category names) on first launch
let storeUrl = FileManager.default.urls(for: .applicationSupportDirectory, in:.userDomainMask).first!.appendingPathComponent(appName + ".sqlite")
let storeUrlFolder = FileManager.default.urls(for: .applicationSupportDirectory, in:.userDomainMask).first!
if !FileManager.default.fileExists(atPath: (storeUrl.path)) {
let seededDataUrl = Bundle.main.url(forResource: appName, withExtension: "sqlite")
let seededDataUrl2 = Bundle.main.url(forResource: appName, withExtension: "sqlite-shm")
let seededDataUrl3 = Bundle.main.url(forResource: appName, withExtension: "sqlite-wal")
try! FileManager.default.copyItem(at: seededDataUrl!, to: storeUrl)
try! FileManager.default.copyItem(at: seededDataUrl2!, to: storeUrlFolder.appendingPathComponent(appName + ".sqlite-shm"))
try! FileManager.default.copyItem(at: seededDataUrl3!, to: storeUrlFolder.appendingPathComponent(appName + ".sqlite-wal"))
}
let storeDescription = NSPersistentStoreDescription(url: storeUrl)
storeDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
//In the view controllers, we'll listen for relevant remote changes
let remoteChangeKey = "NSPersistentStoreRemoteChangeNotificationOptionKey"
storeDescription.setOption(true as NSNumber, forKey: remoteChangeKey)
container.persistentStoreDescriptions = [storeDescription]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
//This is returning nil but I don't think it should
print(storeDescription.cloudKitContainerOptions?.containerIdentifier)
return container
}()
//In my view controller
let context = CoreDataHelper.shared.context
override func viewDidLoad() {
super.viewDidLoad()
//Do other setup stuff, removed for clarity
//enable CloudKit syncing
context.automaticallyMergesChangesFromParent = true
}
override func viewWillAppear(_ animated: Bool) {
clearsSelectionOnViewWillAppear = splitViewController!.isCollapsed
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(
self,
selector: #selector(reportCKchange),
name: NSNotification.Name(
rawValue: "NSPersistentStoreRemoteChangeNotification"),
object: CoreDataHelper.shared.persistentContainer.persistentStoreCoordinator
)
updateUI()
}
#objc
fileprivate func reportCKchange() {
print("Change reported from CK")
tableView.reloadData()
}
Note: I have updated the target to be iOS13+.
I think a newly created NSPersistentStoreDescription has no cloudKitContainerOptions by default.
To set them, try:
storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: <<Your CloudKit ID>>)
I discovered as well that ALL attributes of your entities must be optional, or have a default value, otherwise it just won't work. Be nice if this was clearer in the documentation...
EDIT *********
7-09-2020 1:39 PM PST
I've got what I believe will suffice as a minimal working reproducible version of the app available at:
https://github.com/Rattletrap99/penny-game-test
EDIT *********
I'm building a game in which users create coins as rewards for various achievements. The coins are saved as managed objects in Core Data, with their various attributes. They are retrieved for various reasons, have their attributes modified, etc., during game play.
Everything saves and retrieves perfectly until I quit and relaunch, at which point no data is present in the persistent store. This is the case whether using a simulator or a device.
My usual means of saving to Core Data is:
func saveIt(){
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
appDelegate.saveContext()
}
Which calls:
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
savedFlag += 1
let coinFetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "Coin")
let savedCoins = try context.fetch(coinFetchRequest) as! [Coin]
print("In appDelegate, saveContext, after context.save, there are \(savedCoins.count) coins.")
print("Successfully saved in appDelegate \(String(describing: savedFlag)) times")
} catch {
// 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.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
Every print() statement I put in the code confirms the save, but when I retrieve (upon relaunch), via code similar to:
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let issuerFetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "Issuer")
let coinFetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "Coin")
do {
let issuers = try context.fetch(issuerFetchRequest) as! [Issuer]
print(" ### Upon startup, there are \(issuers.count) Issuers in CD")
let coins = try context.fetch(coinFetchRequest) as! [Coin]
print(" #### Upon startup, there are \(coins.count) Coins in CD")
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
I get:
### Upon startup, there are 0 Issuers in CD
#### Upon startup, there are 0 Coins in CD
Also, I try to save in applicationWillTerminate to be certain the data is saved before quitting:
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground.
// Saves changes in the application's managed object context before the application terminates.
self.saveContext()
let issuerFetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "Issuer")
let coinFetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "Coin")
do {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let issuers = try context.fetch(issuerFetchRequest) as! [Issuer]
let coins = try context.fetch(coinFetchRequest) as! [Coin]
print("\\\\ Upon quit, there are \(issuers.count) Issuers in CD")
print("\\\\ Upon quit, there are \(coins.count) Coins in CD")
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
}
However, the print() statements are not printed, leading me to believe applicationWillTerminate is never fired.
I should mention that Issuer has a to many relationship with Coin, and I ensure that there are Issuers present before creating and saving a Coin.
Any help greatly appreciated!
After beating myself half to death for longer than I care to admit, I found the fault in these three lines in AppDelegate.swift:
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
container.persistentStoreDescriptions = [description]
Once these were removed, everything went back to normal. I'd like to say I understand why, but honestly, removing these lines were an act of desperation. And the irony is that I had added these lines in an attempt to rectify an earlier problem with fetching from Core Data.
Many thanks to all who contributed!
Remember to load up the persistentContainer in your AppDelegate. You can create a project in Xcode with CoreData built-in.
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentContainer(name: "CoreDataModelName")
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
}()
// MARK: - Core Data Saving support
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// 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.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
The line containing NSEntityDescription.entity is crashing and giving me the error
Thread 1: SIGABRT
I have seen other people ask this question, the recommended answer is to simply delete and remake the entity from scratch. I have done this many times, I have also "cleaned" the code thoroughly, and imported CoreData in both my AppDelegate.swift files and this ViewController file. Does anyone have any advice?
override func viewDidLoad() {
super.viewDidLoad()
addGesture()
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
let managedContext = appDelegate.persistentContainer.viewContext
let stringModelEntity = NSEntityDescription.entity(forEntityName: "StringModels", in: managedContext)!
let stringBundle = NSManagedObject(entity: stringModelEntity, insertInto: managedContext)
self.getJSON(stringBundle)
do {
try managedContext.save()
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
}
EDIT
I've found a solution thanks to finally opening the debugger, the following link's 'best answer' describes and solves this issue: Core data: Failed to load model
This will mean that it cannot find the entity with the name "StringModels". In my experience, the error SIGABRT is caused when something that the program thinks should exist does not.
I would check capitalization and spelling.
In my iOS app users complete transactions which I need to post back to the server. I've created a function to do this:
static let configurationParam = NSURLSessionConfiguration.defaultSessionConfiguration()
static var manager = Alamofire.Manager(configuration: configurationParam)
func postItemToServer(itemToPost:DemoItem) {
let webServiceCallUrl = "..."
var itemApiModel:[String: AnyObject] = [
"ItemId": 123,
"ItemName": itemToPost.Name!,
//...
]
ApiManager.manager.request(.POST, webServiceCallUrl, parameters: itemApiModel, encoding: .JSON)
.validate()
.responseJSON { response in
switch response.result {
case .Success:
print("post success")
case .Failure:
print("SERVER RESPONSE: \(response.response?.statusCode)")
}
}
}
Currently I call this once a transaction is complete:
//...
if(transactionCompleted!) {
let apiManager = ApiManager()
apiManager.postItemToServer(self.item)
self.senderViewController!.performSegueWithIdentifier("TransactionCompletedSegue", sender: self)
}
//...
Where DemoItem is a CoreData object.
This all works as expected. However I need the ability to retry the POST request if it fails. For example if the network connection is down at the point of trying post to the server I need to automatically post the data once it becomes active again - at which point there may be several DemoItem's which need to be synced.
I'm new to Swift. In a similar Xamarin app I had a status column in my SQLite database which I set to 'AwaitingSync'. I then had an async timer that ran every 30 seconds, queried the DB for any items which had status='AwaitingSync' and then tried to post them if they existed. If it succeed it updated the status in the DB. I could implement something along the same lines here - but I was never really happy with that implementation as I had a DB query every 30 seconds even if nothing had changed.
Finally, it needs to be still work if the app is terminated. For example any items which weren't synced before the app is killed should sync once the app is resumed. What's the best way to approach this?
Edit
Based on Tom's answer I've created the following:
class SyncHelper {
let serialQueue = dispatch_queue_create("com.mycompany.syncqueue", DISPATCH_QUEUE_SERIAL)
let managedContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
func StartSync() {
//Run on serial queue so it can't be called twice at once
dispatch_async(serialQueue, {
//See if there are any items pending to sync
if let itemsToSync = self.GetItemsToSync() {
//Sync all pending items
for itemToSync in itemsToSync {
self.SyncItemToServer(itemToSync)
}
}
})
}
private func GetItemsToSync() -> [DemoItem]? {
var result:[DemoItem]?
do {
let fetchRequest = NSFetchRequest(entityName: "DemoItem")
fetchRequest.predicate = NSPredicate(format: "awaitingSync = true", argumentArray: nil)
result = try managedContext.executeFetchRequest(fetchRequest) as? [DemoItem]
} catch {
//Handle error...
}
return result
}
private func SyncItemToServer(itemToSync:DemoItem) {
let apiManager = ApiManager()
//Try to post to the server
apiManager.postItemToServer(itemToSync:DemoItem, completionHandler: { (error) -> Void in
if let _ = error {
//An error has occurred - nothing need to happen as it will be picked up when the network is restored
print("Sync failed")
} else {
print("Sync success")
itemToSync.awaitingSync = false
do {
try self.managedContext.save()
} catch {
//Handle error...
}
}
})
}
}
I then call this when ever a transaction is completed:
//...
if(transactionCompleted!) {
let syncHelper = SyncHelper()
syncHelper.StartSync()
}
//...
And then finally I've used Reachability.swift to start the sync every time the network connection resumes:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var reachability:Reachability?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
//...
//Setup the sync for when the network connection resumes
do {
reachability = try Reachability.reachabilityForInternetConnection()
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "reachabilityChanged:",
name: ReachabilityChangedNotification,
object: reachability)
try reachability!.startNotifier()
} catch {
print("Unable to create Reachability")
}
return true
}
func reachabilityChanged(note: NSNotification) {
let reachability = note.object as! Reachability
if reachability.isReachable() {
print("Network reachable")
let syncHelper = SyncHelper()
syncHelper.StartSync()
} else {
print("Not reachable")
}
}
}
This all seems to be working. Is this approach ok and have I missed anything which would improve it? The only gap I can see is if the network connectivity is active however the server throws an error for some reason - I guess I could then add a button for the user to retry any pending items.
Firstly, if your concern is whether the network connection is working, you shouldn't be polling at intervals. You should be using iOS's network reachability API to get notified when the network status changes. Apple provides a simple implementation of this and there are numerous alternative implementations online.
Since a sync status value should be a boolean flag, it's not as if a fetch request is a heavy-duty operation, especially if you use reachability. Not only should the fetch request be fast, you can update the flag after the fact in a single step-- use NSBatchUpdateRequest to set the flag to false on every instance you just sent to the server.
If you want to get the sync status out of the persistent store (not a bad idea since it's metadata), you'll need to maintain your own list of unsynced objects. The best way to do this is by tracking the objectID of the managed objects awaiting sync. That would be something like:
Get the objectID of a newly changed managed object
Convert that to an NSURL using NSManagedObjectID's URIRepresentation() method.
Put the NSURL on a list that you save somewhere, so it'll persist.
You can save the list in a file, in user defaults, or in the persistent store's own metadata.
When it's time to sync, you'd do something like:
Get an NSURL from your list
Convert that into an NSManagedObjectID using managedObjectIDForURIRepresentation(url:NSURL) (which is on NSPersistentStoreCoordinator)
Get the managed object for that ID objectWithID: on NSManagedObjectContext.
Sync that object's data.
Then on a successful sync, remove entries from the list.