I've done some research and have found some decent info on objective-C code, but almost nothing for Swift. I figure this is a pretty common pattern so hopefully we can hammer out how to do it correctly. I've made some pretty significant progress and feel like I'm pretty close, but I'm just out of my depth in Swift.
Goal: Make an app that uses a background thread to parse data and do long fetch requests, and have a main thread that uses an NSFetchedResults Controller.
Code from one of my functions to spin off a new Thread
let tQueue = NSOperationQueue()
let testThread1 = testThread()
tQueue.addOperation(testThread1)
testThread1.threadPriority = 0
testThread1.completionBlock = {() -> () in
println("Thread Completed")
}
Class I made for making a thread
class testThread: NSOperation{
var delegate = UIApplication.sharedApplication().delegate as AppDelegate
var threadContext:NSManagedObjectContext?
init(){
super.init()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "contextDidSave:", name: NSManagedObjectContextDidSaveNotification, object: nil)
}
override func main(){
self.threadContext = NSManagedObjectContext()
threadContext!.persistentStoreCoordinator = delegate.persistentStoreCoordinator
...
//Code that actually does a fetch, or JSON parsing
...
threadContext!.save(nil)
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func contextDidSave(notification: NSNotification){
let sender = notification.object as NSManagedObjectContext
if sender !== self.threadContext{
self.threadContext!.mergeChangesFromContextDidSaveNotification(notification)
}
}
}
I won't include all the code for the NSFetchedResultsController, but I have one linked to the main context. When my threading is commented out the app runs fine, it will block the UI and parse/fetch the data that needs to be inserted into core data, and when it is all complete the UI will unlock.
When I add the threading, as soon as I do anything in the UI that could trigger a save to the main context (in this case, the tappedOnSection table function performs a save), the app crashes and the only thing that appears in the console is. "lldb". The line that is highlighted that triggered the error was
managedObjectContext?.save(nil)
The error next to it is "EXC_BAD_ACCESS(code 1, address=...
If I simply wait for the background thread to complete, upon completion, I get an error also this time tracing to the "didChangeObject" method of the NSFetchedResultsController. It says "unexpectedly found nil while unwrapping an optional value, and flags the following case:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch(type){
... other cases
case NSFetchedResultsChangeType.Update:
self.configureCell(self.tableView.cellForRowAtIndexPath(indexPath!)!, atIndexPath: indexPath!)
...other cases
}
}
I'm assuming I'm having some problem with concurrency that I'm not handling properly. I thought that the NSNotification that watched for changes handles this, but I must be missing something else.
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "contextDidSave:", name: NSManagedObjectContextDidSaveNotification, object: nil)
...
//Code here calls the function that starts the thread shown previously to do a background fetch
}
func contextDidSave(notification: NSNotification){
let sender = notification.object as NSManagedObjectContext
if sender !== self.managedObjectContext!{
println("Save Detected Outside Thread Main")
self.managedObjectContext!.mergeChangesFromContextDidSaveNotification(notification)
}
}
UPDATE:
With some help from you guys I've been able to localize the error. It seems that the didChangeObject method from the NSFetchedResultsController is the problem. If the data changes, or new rows are inserted, the didChange Object method fires off respective methods to perform these animations, its here where I get the nil errors. Obviouslly, the whole point is that when the background data is fetched, that it will just smoothly animate in, but instead of doing this, it blows up. If I comment this function out, than I don't get any errors, but also loose the smooth animation I hope for. The attached didChangeObject method is below. It is mostly straight from the swift documentation on NSFetchedResultController:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch(type){
case NSFetchedResultsChangeType.Insert:
self.tableView.insertRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
case NSFetchedResultsChangeType.Delete:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
case NSFetchedResultsChangeType.Update:
if self.tableView.cellForRowAtIndexPath(indexPath!) != nil{
self.configureCell(self.tableView.cellForRowAtIndexPath(indexPath!)!, atIndexPath: indexPath!)
}
case NSFetchedResultsChangeType.Move:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
self.tableView.insertRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
}
}
In the end I ended up researching the many different ways to do threading. The most useful turned out to be the Multiple contexes, with the Notification center as described here I also implemented the multiple contexts solution, but degraded back to the other in the end. My issue turned out to be that I was sharing a delegate between multiple NSFetchedResultsControllers without checking if the incoming controller was the same as the one that the table was currently using. This produced out of bounds errors whenever the data auto-reloaded.
My background thread solution was simple.
Call the background context using the performBlock
context.performBlock {
//background code here
Listen for changes with the main context.
NSNotificationCenter.defaultCenter().addObserver(self, selector: "contextDidSave:", name: NSManagedObjectContextDidSaveNotification, object: nil)
func contextDidSave(notification: NSNotification) {
let sender = notification.object as NSManagedObjectContext
if sender != managedObjectContext {
managedObjectContext!.mergeChangesFromContextDidSaveNotification(notification)
}
}
Merge the changes into the mainContext.
My initial setup was actually quite close to correct, what I didn't know was that when you setup your backgroundContext you can give it a concurrency type
let childContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
then you can call the context in a background thread using context.performBlock (as shown above) whenever you want to do background thread stuff.
Also, the NSFetchedResultsController used the mainContext as their context so that they wouldn't get blocked during parsing.
UPDATE
There are multiple ways to do background threads, the above solution is just one. Quellish describes another popular approach in his article here. It is very informative and I recommend it, it describes the nested context approach to the queue confinement.
You should check whether self.tableView.cellForRowAtIndexPath
is nil before calling self.configureCell...
- it can be nil if the relevant row is no longer visible.
The FRC delegate methods ought to pick up the requisite changes to the tableView, but given you may have many updates coming in from the background, you could just put a reloadData
in the controllerDidChangeContent:
method.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With