Store URLSessionDownloadTask in Core Data - ios

I want to save URLSessionDownloadTask in core data when app is gone in closed state or my download state is changed e.g from waiting state to downloading state or to completed state.
All other attributes of my custom class are stored perfectly but app crashes when it stores download task.
reason to crash is
[__NSCFLocalDownloadTask encodeWithCoder:]: unrecognized selector sent to instance 0x7ff189f181c0
-[NSKeyedArchiver dealloc]: warning: NSKeyedArchiver deallocated without having had -finishEncoding called on it.
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFLocalDownloadTask encodeWithCoder:]: unrecognized selector sent to instance 0x7ff189f181c0'
this is my class
class VideoDownloadModel : NSManagedObject {
#NSManaged var videoID : NSNumber?
#NSManaged var vid : Video?
#NSManaged var downloadTask : URLSessionDownloadTask?
#NSManaged var downloadStatus : String?
}
storing it like this
let request = NSFetchRequest<NSFetchRequestResult>(entityName: (COREDATA_ENTITY_Description?.name)!)
request.returnsObjectsAsFaults = false
request.predicate = NSPredicate(format: "videoID == %#", videoModel.videoID!)
do {
let result = try COREDATA_CONTEXT.fetch(request)
print(result)
var vidArr = result as! [VideoDownloadModel]
if vidArr.count != 0 {
vidArr[0] = videoModel
COREDATA_MANAGER.saveContext()
}
} catch {
let fetchError = error as NSError
print(fetchError)
}
when URLSessionDownlaodTask is nil it works fine but when any download is started it crashes on saving.
scenerio :
I initialized my custom class object with all attributes but set task to nil.
I store that object in core data it saved perfectly.
I initialize the task of that object the download work perfectly.
Then i update the object in core data while updating the app got crash because URLSssionTask is not inheriting from NSCoding. so it don't have encoding and decoding methods.
I want some solution to solve this issue
Any help will be appreciated. Thanks.

You can't save the URLSessionDownloadTask in Core Data because-- as you mentioned-- it doesn't conform to NSCoding. In many cases the answer would be to write your own code to convert to/from Data but that doesn't work in this case. A URLSessionDownloadTask can only be created by a URLSession, so you can't serialize and deserialize the task object.
That doesn't really matter though because saving and restoring them doesn't make sense. A URLSessionDownloadTask represents something that is in progress while the app is running. If your app is closed, that activity ends. Restoring a URLSessionDownloadTask after the app closes and relaunches doesn't make sense because there's no activity for it to represent. Basically, even if you could store the task object and restore it, it would be useless after restoring. There's no reason to bother.
If your interest is that you want to resume incomplete downloads, you'll have to start over from the beginning. Create your URLSession and then use it to create a new URLSessionDownloadTask. If your interest is in getting information about a background download, you can use the session object with getTasksWithCompletionHandler(_:) to find out whether they completed.

Saving URLSessionDownloadTask doesn't make sense. You would actually want to save the data obtained from downloadTask.cancel(byProducingResumeData: ) while pausing.
Once you need to resume the download create a new downloadtask with the saved data by using downloadTaskWithResumeData(:) and resume.
When app is terminated - dosen't include the case when the app is forcefully terminated by the user.
The apple documentation on URLSession clearly explains what to do when app is terminated.
From the docs - In both iOS and OS X, when the user relaunches your app, your app should immediately create background configuration objects with the same identifiers as any sessions that had outstanding tasks when your app was last running, then create a session for each of those configuration objects. These new sessions are similarly automatically reassociated with ongoing background activity.
When user forcefully terminate the app
In this case, URLSession delegate urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) will be fired, in which the userInfo dict in error object will have the resume data corresponding to the key NSURLSessionDownloadTaskResumeData, which could be used to resume the task using downloadTask.cancel(byProducingResumeData: ). Also please note that you will have to typecast error to NSError to retrieve the userInfo dict.
It would be good to read through the docs here before using NSURLSession

Related

iOS Core Data - Serious application error - attempt to insert nil - in less than 1%

iOS Core Data - Serious application error - attempt to insert nil
Hello,
My app runs actualy stable, but in seldom cases it crashes with this error message...
2019-04-02 20:48:52.437172+0200 myAppName[4422:1595677] [error] error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. -[__NSCFSet addObject:]: attempt to insert nil with userInfo (null)
CoreData: error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. -[__NSCFSet addObject:]: attempt to insert nil with userInfo (null)
2019-04-02 20:48:52.438246+0200 myAppName[4422:1595677] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFSet addObject:]: attempt to insert nil'
...when it tries to save the current context (this part in my code is still in objc):
- (void)saveChanges
{
dispatch_async(dispatch_get_main_queue(), ^{
NSError *err = nil;
BOOL succesful = [self->context save:&err];
if (!succesful)
{
NSLog(#"ERROR MESSAGE DURING SAVING CONTEXT: %#", [err localizedDescription]);
}
});
}
'seldom' means:
Most Customers do never experience the issue, for few customers it happens several times per day.
I was able to produce it 2 times during the last two days although I tried several ways to force this error (see below).
This is the setup:
The respective data is in one Entity (table)
A NSFetchedResultsController shows the data in an UITableView
User can hit a button to add a new record.
New record has only some basic data and initiates two API calls to two webservers
Each webserver response does update the record
After both are done (or were cancelled due to timeout), I call the saveChanges function from above only once.
All functions use the same context created by NSPersistentContainer as follow (this part is already in swift)
#objc lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "myAppName")
let description = NSPersistentStoreDescription(url: SomeHelper.urlForFileInDocFolder("storev7.data"))
container.persistentStoreDescriptions = [description]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
If I could reproduce the error somehow, I could find an appropriate solution, but as it almost never happens, I'm stuck.
Do you have an idea how I could reproduce the error from above? Or do you have a clue what could cause the error in my case?
What I tried already to reproduce the error:
Create hundereds of record
Create hundereds of record in a few seconds
Create hundereds of record during switching internet connection on / off / on / off /...
Create hundereds of record during mixed from background and main thread (I removed the dispatch from saveChanges for that)
Create hundereds of record with different delays on the API (added random sleep function on the webserver)
Long time execution, the app run for 24 hours on a real device and created record each 2 minutes
Mixes of all of them
NSManagedObjects are restricted to a single queue. They are not thread-safe for reading or writing. Reading an NSManagedObject can cause a fault, which is a write operation. That means that NSManagedObjects retrieved from a main queue context (like viewContext) cannot be passed to other queues.
The details of all of this are discussed in the Core Data Programming Guide:
NSManagedObject instances are not intended to be passed between queues. Doing so can result in corruption of the data and termination of the application. When it is necessary to hand off a managed object reference from one queue to another, it must be done through NSManagedObjectID instances.
The general approach with NSPersistentContainer is to use something like viewContext exclusively on the main queue, and to use performBackgroundTask to handle background operations, or you can use newBackgroundContext to generate a background context, and use perform or performAndWait on it to manipulate objects that are fetched from that context.
Moving an object between contexts is done by fetching the same objectID in the other context (keeping in mind that this will return a fresh instance from the store).
You can track down mistakes by adding -com.apple.CoreData.ConcurrencyDebug 1 to your scheme. When you do this, errors will immediately trap on the delightfully named __Multithreading_Violation_AllThatIsLeftToUsIsHonor__.

Having to call fetch twice from CoreData

Both on simulator and my real device, an array of strings is saved upon app termination. When I restart the app and fetchRequest for my persisted data (either from a viewDidLoad or a manual button action), I get an empty array on the first try. It isn't until the second time I fetchRequest that I finally get my data.
The funny thing is that there doesn't seem to be a time discrepancy involved in this issue. I tried setting various timeouts before trying to fetch the second time. It doesn't matter whether I wait 10 seconds to a minute -- or even immediately after the first fetch; the data is only fetched on the second try.
I'm having to use this code to fetch my data:
var results = try self.context.fetch(fetchRequest) as! [NSManagedObject]
while (results.isEmpty) {
results = try self.context.fetch(fetchRequest) as! [NSManagedObject]
}
return results
For my sanity's sake, here's a checklist:
I'm initializing the Core Data Stack using boilerplate code from Apple: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreData/InitializingtheCoreDataStack.html#//apple_ref/doc/uid/TP40001075-CH4-SW1
I'm putting my single DataController instance in a static variable at the top of my class private static let context: NSManagedObjectContext = DataController().managedObjectContext
I'm successfully saving my context and can retrieve the items without any issue in a single session; but upon trying to fetch on the first try in a subsequent session, I get back an empty array (and there lies the issue).
Note** I forgot to mention that I'm building a framework. I am using CoreData with the framework's bundle identifier and using the model contained in the framework, so I want to avoid having to use logic outside of the framework (other than initalizing the framework in the appDelegate).
The Core Data stack should be initialized in applicationDidFinishLaunchingWithOptions located in appDelegate.swift because the psc is added after you're trying to fetch your data.
That boilerplate code from Apple includes:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
/* ... */
do {
try psc.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil)
} catch {
fatalError("Error migrating store: \(error)")
}
}
The saved data isn't available until the addPersistentStoreWithType call finishes, and that's happening asynchronously on a different queue. It'll finish at some point but your code above is executing before that happens. What you're seeing isn't surprising-- you're basically looping until the async call finishes.
You need to somehow delay your fetch until the persistent store has been loaded. There are a couple of possibilities:
Do something sort of like what you're already doing. I'd prefer to look at the persistent store coordinator's persistentStores property to see if any stores have been loaded rather than repeatedly trying to fetch.
Post a notification after the persistent store is loaded, and do your fetch when the notification happens.

Core Data + iCloud Sync did change stores notification doesn't refresh UI

I can't handle this problem, I'm trying to integrate iCloud and Core Data to my app and I stuck with iCloud synchronization part.
My complete scenario:
Local core data storage seeded with initial data
Later the app ask the user about iCloud or local data stoge
If user choose iCloud, current local storage migrates to iCloud store
After migration, context and persistent store coordinator is reloaded with iCloud Store
Refetch data from new context (Trouble here)
If we take away discussion about migration and focus on loading persistent store coordinator with iCloud, I think that the problem is related to
NSPersistentStoreCoordinatorStoresDidChangeNotification event.
I just don't understand it. In every article that I read, I saw something like: "reload you UI here". But it doesn't work for me.
Reload coordinator function
func configureContext() -> NSManagedObjectContext? {
// Create the coordinator and context
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
//remove all observers from "previous" coordinator
deregisterForStoreChanges()
//and register them for new one
registerObservers(persistentStoreCoordinator: coordinator)
do {
//storeSettings.url = applicationDocumentsDirectory.URLByAppendingPathComponent("iCloud.sqlite")
//storeSettings.options = [NSPersistentStoreUbiquitousContentNameKey:"iCloudProject"]
try coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeSettings.url, options: storeSettings.options)
} catch {
//Here was standard error handler I didn't changed it, but removed for listing
abort()
}
persistentStoreCoordinator = coordinator
let managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = coordinator
//not quite sure about the next string, didn't test it yet
managedObjectContext.mergePolicy = NSMergePolicy(mergeType:NSMergePolicyType.MergeByPropertyStoreTrumpMergePolicyType )
//tried to refetch my records here but, with no luck.
//refetchUIRecords()
return managedObjectContext
}
And my NSPersistentStoreCoordinatorStoresDidChangeNotification event func
func persistentStoreCoordinatorDidChangeStores(notification:NSNotification){
if notification.userInfo?[NSAddedPersistentStoresKey] != nil {
print("-----------------------")
print("ADDED")
print("-----------------------")
//deduplicateRecords()
//if iCloud -> refetch UI
if NSUserDefaults.standardUserDefaults().boolForKey("iCloud"){
self.createTimer()
}
}
print("Did Change")
}
Create timer function is just a function which wait for 1 secods before actual refetching and refreshing UI.
The problem
When we reach step 4 from my scenario, and call configureContext function,
I see this in the console:
2016-04-12 13:31:27.749 TestingiCloudProject[2052:1142902] -[PFUbiquitySwitchboardEntryMetadata setUseLocalStorage:](898): CoreData: Ubiquity: mobile~567404C0-9D84-4C07-A0F8-D25832CB65D8:iCloudProject
Using local storage: 1 for new NSFileManager current token <ef7a917f bca47ecf 5d58862d cbe9998d 7e53e5ea>
Did Change
Did Change
-----------------------
ADDED
-----------------------
Timer
Did Change
Refetch
Refetching...
Number of records after fetch: 1 //must be more than 1
2016-04-12 13:31:30.090 TestingiCloudProject[2052:1143055] -[PFUbiquitySwitchboardEntryMetadata setUseLocalStorage:](898): CoreData: Ubiquity: mobile~567404C0-9D84-4C07-A0F8-D25832CB65D8:iCloudProject
Using local storage: 0 for new NSFileManager current token <ef7a917f bca47ecf 5d58862d cbe9998d 7e53e5ea>
As you can see my fetch request executes before Using local storage: 0 and that's why I'm recieving records from the local storage (1 record) not iCloud (which contains more than 1).
I don't understand when to refetch my records. I take timer part from this great source for all who want to know more about Core Data and iCloud.
But, 1 second isn't enought, 2 seconds is work as I want, but what if iCloud store will be bigger than mine, or network connection will be worse than mine, I don't think that timer is the way.
I hope somebody already face this trivial problem.
EDIT
I didn't find any help from Apple dev forum and SO, so I activated my code tech support token, I hope they can help me. As soon as I'll solve my problem I'll write an answer. But, if you, who read this question, know the possible answer, post it now.
There is another notification NSPersistentStoreDidImportUbiquitousContentChangesNotification that is fired by the persistent store coordinator whenever data is imported from the ubiquitous content store. Here you can merge the changes with your NSManagedObjectContext.
When the ubiquity container receives changes from iCloud, Core Data posts an NSPersistentStoreDidImportUbiquitousContentChangesNotification notification. This notification’s userInfo dictionary is structured similarly to that of an NSManagedObjectContextDidSaveNotification notification except for that it contains NSManagedObjectID instances rather than NSManagedObject instances. Therefore you can merge in changes from other peers in the same way that you merge changes from other managed object contexts. Call mergeChangesFromContextDidSaveNotification: on your managed object context, passing in the notification object posted by Core Data.
https://developer.apple.com/library/ios/documentation/DataManagement/Conceptual/UsingCoreDataWithiCloudPG/UsingSQLiteStoragewithiCloud/UsingSQLiteStoragewithiCloud.html

Call ExtensionDelegate to create/refresh data for Complication

All my data creation is done in the ExtensionDelegate.swift.
The problem is ExtensionDelegate.swift doesn't get called before the function getCurrentTimelineEntryForComplication in my ComplicationController.swift.
Any ideas? Here is my code and details:
So my array extEvnts is empty in my ComplicationController.swift:
func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {
let extEvnts = ExtensionDelegate.evnts
}
Because my ExtensionDelegate.swift hasn't gotten called yet, which is what creates the data for the array:
class ExtensionDelegate: NSObject, WKExtensionDelegate, WCSessionDelegate {
private let session = WCSession.defaultSession()
var receivedData = Array<Dictionary<String, String>>()
static var evnts = [Evnt]()
func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
if let tColorValue = userInfo["TeamColor"] as? String, let matchValue = userInfo["Matchup"] as? String {
receivedData.append(["TeamColor" : tColorValue , "Matchup" : matchValue])
ExtensionDelegate.evnts.append(Evnt(dataDictionary: ["TeamColor" : tColorValue , "Matchup" : matchValue]))
} else {
print("tColorValue and matchValue are not same as dictionary value")
}
}
func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
if WCSession.isSupported() {
session.delegate = self
session.activateSession()
}
}
}
EDIT:
Per Apple, it looks like this has something to do with it, but for some reason I have no idea how to actually implement it because I'm not able to call mydelegate.evnts:
// Get the complication data from the extension delegate.
let myDelegate = WKExtension.sharedExtension().delegate as! ExtensionDelegate
var data : Dictionary = myDelegate.myComplicationData[ComplicationCurrentEntry]!
So I've tried something like this, and still can't get it working because I'm still getting no data:
func someMethod() {
let myDelegate = WKExtension.sharedExtension().delegate as! ExtensionDelegate
let dict = ExtensionDelegate.evnts
print("ExtensionDel.evnts: \(dict.count)")
}
Useful question that helped me here
In the function requestedUpdateDidBegin() you can update the information that you will display in your complication. So in this method you may make a call to your parent app using a WatchConnectivity method like sendMessage:replyHandler:errorHandler: to receive new information.
You can use NSUserDefaults to store your imperative data that will be used in your ComplicationController, then load this information from NSUserDefaults for your complication. I store this data in user defaults so that I always have old data to display in case the new data fails to load.
TL/DR: Have the extension tell ClockKit to update the complication after the data is received.
First issue:
So my array extEvnts is empty in my ComplicationController.swift ... Because my ExtensionDelegate.swift hasn't gotten called yet, which is what creates the data for the array
Your array is empty because the data hasn't been received at that point.
You can't (get the complication controller to) force the watch (extension) to receive data which may not have even been transmitted yet.
If you look at the WCSession Class Reference, transferUserInfo queues data to be transferred in the background, when the system decides it's a good time to send the info.
Remember that background transfers are not be delivered immediately. The system sends data as quickly as possible but transfers are not instantaneous, and the system may delay transfers slightly to improve power usage. Also, sending a large data file requires a commensurate amount of time to transmit the data to the other device and process it on the receiving side.
Second issue:
You're trying to combine updating your app and your complication based on data sent from your phone. But your app and your complication don't necessarily run together. It's not surprising or unexpected that the watch updates the complication before any data has even been sent/received. The App Programming Guide for watchOS mentions that
Complications exist entirely in the WatchKit extension. Their user interface is not defined in the Watch app. Instead, it is defined by an object implementing the CLKComplicationDataSource protocol. When watchOS needs to update your complication, it launches your WatchKit extension. However, the Watch app’s executable is not launched.
There's no mechanism for the complication controller to say, "Wait, I'm not ready to provide an update. The complication controller can't wait on (or as mentioned, force) the watch extension to receive data.
It's only responsibility is to immediately return data based on what's currently available to it. If there's no data, it must return an empty timeline.
Approaching this problem:
You shouldn't necessarily think of app updates and complication updates as the same thing. The first is not budgeted, but the second is budgeted. If you update your complication too often, you may exceed your daily budget, and no further updates will occur for the remainder of the day.
Complications aren't meant to be frequently updated. Complications should provide as much data as possible during each update cycle. You shouldn't ask the system to update your complication within minutes. You should provide data to last for many hours or for an entire day.
Having covered that, you could wait until your extension has received data, then can ask ClockKit to extend your timeline, so new entries can be added to it. extendTimelineForComplication: is documented in the CLKComplicationServer Class reference.
As an aside, if your data is urgent, you should use transferCurrentComplicationUserInfo. It's a high-priority message, which is placed at the head of the queue, and the extension is woken up to receive it. See this answer for a comparison between it and transferUserInfo.
You also could setup a singleton to hold your data which the watch app and complication controller both use. This was mentioned in an answer to an old question of yours, and also recommended by an Apple employee on the developer forums.

iOS NSURLSession handle the completion of a data task with a custom delegate

My end goal is to make http requests in the background while handling the response in memory. From my understanding, background requests have to be made with a custom delegate (which means I can't use dataTaskWithRequest(request, completionHandler)), and to deal with the response in memory I have to use a data task (which means I can't use a download task along with URLSession(session, downloadTask, didFinishDownloadingToURL)).
According to this: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/URLLoadingSystem/NSURLSessionConcepts/NSURLSessionConcepts.html#//apple_ref/doc/uid/10000165i-CH2-SW1 it doesn't look like there are any delegate methods that get called for data tasks upon completion. Is the only way to work with the response through a delegate to work with individual NSData fragments through URLSession(session, dataTask, data)? https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSessionDataDelegate_protocol/index.html#//apple_ref/occ/intfm/NSURLSessionDataDelegate/URLSession:dataTask:didReceiveData: There's no delegate method for handling the entire final response as a single NSData instance?
For small quantities of data like API calls or small images like avatars meant to be displayed immediately, dataTaskWithRequest(request, completionHandler) is the method you want. It sets up an asynchronous task, meaning that when you start the task, execution will return to your code immediately and the task will take care of downloading the data and buffering it in memory "in the background" while your app is running. Once all the download task is complete, it will call your completionHandler to let it know that it's done and that the data is ready. It will pass the data to your handler as an argument.
For larger files like podcasts, videos and large images, you'll want iOS to download the file for you even when the user starts looking at another app and your app is suspended. You will then want to use a NSURLSessionDownloadTask with a background session configuration backgroundSessionConfigurationWithIdentifier: and a custom delegate. Your custom delegate will need to implement the method URLSession:downloadTask:didFinishDownloadingToURL:. When this method is called, you can read the contents of the file at the url that will be passed to you using code like this:
let data = NSData(contentsOfURL: url)
The reason background downloads that persist after an iOS app is quit are handled like this is that iOS wants to be able to continue downloading multiple files on behalf of different apps like podcasts, videos, etc. If the user is on a high speed network, downloading multiple large files in memory can quickly consume all of the devices memory, so they get stored as they are downloaded instead. In the same vein, you should keep your file sizes in mind before reading the entire file into memory with NSData(contentsOfURL:).
Here's a working example of how everything fits together. Paste this in an iOS playground and look at the image you'll get:
import UIKit
class MyDelegate: NSObject, NSURLSessionDelegate, NSURLSessionDownloadDelegate {
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
if let data = NSData(contentsOfURL: location) {
// work with data ...
UIImage(data: data)
}
}
}
let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier("my-session-identifier")
let delegate = MyDelegate()
let session = NSURLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)
let url = NSURL(string: "https://pbs.twimg.com/profile_images/473550724433858560/tuHsaI2U.png")!
let task = session.downloadTaskWithURL(url)
task.resume()
// this allows the code to run in a playground
import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
If I correctly understand the docs you're supposed to implement the
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
method from URLSessionTaskDelegate. It "tells the delegate that the task finished transferring data" (per docs).
It will be called with nil value for error, but you also have to check task.response for potential problems.
Before that you should manually collect pieces of your data coming via the URLSession:dataTask:didReceiveData: calls:
This delegate method may be called more than once, and each call provides only data received since the previous call. The app is responsible for accumulating this data if needed.
There's no delegate methods that would combine all the data into a single object for you, you have to do it manually.

Resources