Multi-threaded progress bar concurrency issue outside the main view controller - ios

I've found so many solutions for progress bar update within the same thread and view controller, however they seemed to be not similar cases as mine.
In my application, the main view controller calls loadIntoCoreData()(implemented in class MyLoadingService) which asynchronously loads data into core data by another thread. This function has to continuously update the loading percentage (which is written in NSUserDefaults.standardUserDefaults()) to the main thread so that it could be shown on the progress bar in main view controller. I had ever used a while loop in MainViewController to continuously fetch the current percentage value, like below:
class MainViewController {
override func viewDidLoad() {
MyLoadingService.loadIntoCoreData() { result in
NSUserDefaults.standardUserDefaults().setBool(false, forKey: "isLoading")
// do something to update the view
}
self.performSelectorInBackground("updateLoadingProgress", withObject: nil)
}
func updatingLoadingProgress() {
let prefs = NSUserDefaults.standardUserDefaults()
prefs.setBool(true, forKey: "isLoading")
// here I use a while loop to listen to the progress value
while(prefs.boolForKey("isLoading")) {
// update progress bar on main thread
self.performSelectorOnMainThread("showLoadingProcess", withObject: nil, waitUntilDone: true)
}
prefs.setValue(Float(0), forKey: "loadingProcess")
}
func showLoadingProcess() {
let prefs = NSUserDefaults.standardUserDefaults()
if let percentage = prefs.valueForKey("loadingProcess") {
self.progressView.setProgress(percentage.floatValue, animated: true)
}
}
}
And in the class of function loadIntoCoreData:
class MyLoadingService {
let context = (UIApplication.sharedApplication()delegate as! AppDelegate).managedObjectContext!
func loadIntoCoreData(source: [MyModel]) {
var counter = 0
for s in source {
//load into core data using the class context
NSOperationQueue.mainQueue.addOperationWithBlock({
// updating the value of "loadingProcess" in NSUserDefaults.standardUserDefaults()
// and synchronize it on main queue
})
counter++
}
}
}
The above code can successfully run the progress bar, however it often encounter BAD_ACCESS or some other exceptions(like "Cannot update object that was never inserted") due to the conflicts on core data context (thought it seems that managedObjectContext isn't touched by the main thread). Therefore, instead of using a while loop listening on the main thread, I consider using NSOperationQueue.performSelectorOnMainThread to acknowledge the main thread after each entry. Therefore I put my view controller as an argument sender into loadCoreData and call performSelectorOnMainThread("updateProgressBar", withObject: sender, waitUntilDone: true) but failed with error "unrecognized selector sent to class 'XXXXXXXX'". So I would like to ask if is it possible to update an UI object between threads? Or, how to modify my previous solution so that the core data context conflicts could be solved? Any solutions are appreciated.
class MyLoadingService {
func loadIntoCoreData(sender: MainViewController, source: [MyModel]) {
var counter = 0
for s in source {
//load into core data using the class context
NSOperationQueue.mainQueue.addOperationWithBlock({
// updating the value of "loadingProcess" in NSUserDefaults.standardUserDefaults()
// and synchronize it on main queue
})
NSOperationQueue.performSelectorOnMainThread("updateProgressBar", withObject: sender, waitUntilDone: true)
counter++
}
}
func updateProgressBar(sender: MainViewController) {
sender.progressView.setProgress(percentage, animated: true)
}
}
class MainViewController {
override func viewDidLoad() {
MyLoadingService.loadIntoCoreData(self) { result in
// do something to update the view
}
}
}

First, you are abusing NSUserDefaults in horrible ways. The documentation describes it as this...
The NSUserDefaults class provides a programmatic interface for
interacting with the defaults system. The defaults system allows an
application to customize its behavior to match a user’s preferences.
For example, you can allow users to determine what units of
measurement your application displays or how often documents are
automatically saved. Applications record such preferences by assigning
values to a set of parameters in a user’s defaults database. The
parameters are referred to as defaults since they’re commonly used to
determine an application’s default state at startup or the way it acts
by default.
You are using it to store a global variable.
Furthermore, you are completely abusing the user's CPU in your loop where you continuously are checking the value in the user defaults, and clipping off a selector to the main thread. "Abuse of the CPU" doesn't even come close to describing what this code is doing.
You should use NSProgress for reporting progress. There is a WWDC 2015 presentation dedicated exclusively to using NSProgress.
On to your core data usage.
Unfortunately, since you intentionally redacted all of the core data code, it's impossible to say what is going wrong.
However, based on what I see, you are probably trying to use that managed object context from your app delegate (which is probably still created with the deprecated confinement policy) from a background thread, which is a cardinal sin of the highest order as far as core data is concerned.
If you want to import data as a long running operation, use a private context, and execute the operations in the background. Use NSProgress to communicate progress to anyone wanting to listen.
EDIT
Thanks for the advice on my core data context usage. I digged into all
the contexts in my code and re-organized the contexts inside, the
conflict problem does not happen anymore. As for NSProgress , it's a
pity that the WWDC presentation focus on the feature on iOS 9 (while
my app must compact on iOS 8 devices). However, even though I use
NSProgress, I should still tell the main thread how many data the core
data (on another thread) already has, right? How does the thread on
NSProgress know the loading progress on my core data thread? –
whitney13625
You can still use NSProgress for iOS8, then only real difference is that you can't explicitly add children, but the implicit way still works, and that video explains it as well.
You really should watch the whole video and forget about the iOS9 part, except to know that you must add children implicitly instead of explicitly.
Also, this pre-iOS9 blog post should clear up any questions you have about it.

Related

UI not updating (in Swift) during intensive function on main thread

I wondered if anyone could provide advice on how I can ‘force’ the UI to update during a particularly intensive function (on the main thread) in Swift.
To explain: I am trying to add an ‘import’ feature to my app, which would allow a user to import items from a backup file (could be anything from 1 - 1,000,000 records, say, depending on the size of their backup) which get saved to the app’s CodeData database. This function uses a ‘for in’ loop (to cycle through each record in the backup file), and with each ‘for’ in that loop, the function sends a message to a delegate (a ViewController) to update its UIProgressBar with the progress so the user can see the live progress on the screen. I would normally try to send this intensive function to a background thread, and separately update the UI on the main thread… but this isn't an option because creating those items in the CoreData context has to be done on the main thread (according to Swift’s errors/crashes when I initially tried to do it on a background thread), and I think this therefore is causing the UI to ‘freeze’ and not update live on screen.
A simplified version of the code would be:
class CoreDataManager {
var delegate: ProgressProtocol?
// (dummy) backup file array for purpose of this example, which could contain 100,000's of items
let backUp = [BackUpItem]()
// intensive function containing 'for in' loop
func processBackUpAndSaveData() {
let totalItems: Float = Float(backUp.count)
var step: Float = 0
for backUpItem in backUp {
// calculate Progress and tell delegate to update the UIProgressView
step += 1
let calculatedProgress = step / totalItems
delegate?.updateProgressBar(progress: calculatedProgress)
// Create the item in CoreData context (which must be done on main thread)
let savedItem = (context: context)
}
// loop is complete, so save the CoreData context
try! context.save()
}
}
// Meanwhile... in the delegate (ViewController) which updates the UIProgressView
class ViewController: UIViewController, ProgressProtocol {
let progressBar = UIProgressView()
// Delegate function which updates the progress bar
func updateProgressBar(progress: Float) {
// Print statement, which shows up correctly in the console during the intensive task
print("Progress being updated to \(progress)")
// Update to the progressBar is instructed, but isn't reflected on the simulator
progressBar.setProgress(progress, animated: false)
}
}
One important thing to note: the print statement in the above code runs fine / as expected, i.e. throughout the long ‘for in’ loop (which could take a minute or two), the console continuously shows all the print statements (showing the increasing progress values), so I know that the delegate ‘updateProgressBar’ function is definitely firing correctly, but the Progress Bar on the screen itself simply isn’t updating / doesn’t change… and I’m assuming it’s because the UI is frozen and hasn’t got ‘time’ (for want of a better word) to reflect the updated progress given the intensity of the main function running.
I am relatively new to coding, so apologies in advance if I ask for clarification on any responses as much of this is new to me. In case it is relevant, I am using Storyboards (as opposed to SwiftUI).
Just really looking for any advice / tips on whether there are any (relatively easy) routes to resolve this and essentially 'force' the UI to update during this intensive task.
You say "...Just really looking for any advice / tips on whether there are any (relatively easy) routes to resolve this and essentially 'force' the UI to update during this intensive task."
No. If you do time-consuming work synchronously on the main thread, you block the main thread, and UI updates will not take effect until your code returns.
You need to figure out how to run your code on a background thread. I haven't worked with CoreData in quite a while. I know it's possible to do CoreData queries on a background thread, but I no longer remember the details. That's what you're going to need to do.
As to your comment about print statements, that makes sense. The Xcode console is separate from your app's run loop, and is able to display output even if your code doesn't return. The app UI can't do that however.

Deleting CoreData object results in EXC_BAD_INSTRUCTION

I have a list-based app. To support adding new elements to a list, what I do is that in the action handler, I create a new object, and I pass this to a details view. If the user cancels the details view, I want to delete the object - when I do this, I get an EXC_BAD_INSTRUCTION.
There must be some threading issue going on.
If I delete the note immediately after creating it (as a test), it works:
Button(action: {
self.newNote = NoteDataManager.makeNote(moc: self.moc, folder: self.savingFolder)
// DELETE immediately, as a test
self.moc.delete(self.newNote!)
self.showNewNoteView = true
}) {
Image(systemName: "square.and.pencil")
}
.sheet(isPresented: self.$showNewNoteView) {
CreateNoteView(newNote: self.newNote!, cancelAction: {
// DO NOTHING here (for now)
})
}
However, if I delete it in the handler for cancelAction (which is called when the user taps a cancel button), I get the exception:
Button(action: {
self.newNote = NoteDataManager.makeNote(moc: self.moc, folder: self.savingFolder)
self.showNewNoteView = true
}) {
Image(systemName: "square.and.pencil")
}
.sheet(isPresented: self.$showNewNoteView) {
CreateNoteView(newNote: self.newNote!, cancelAction: {
// MOVING the delete action here causes EXC_BAD_INSTRUCTION
self.moc.delete(self.newNote!)
})
}
My guess is that it's some sort of threading issue or something. Has anyone run into this and know how to work around this issue? It seems like it should work ...
Core Data is designed to work in a multithreaded environment. However, not every object under the Core Data framework is thread safe. To use Core Data in a multithreaded environment, ensure that:
Managed object contexts are bound to the thread (queue) that they are associated with upon initialization.
Managed objects retrieved from a context are bound to the same queue that the context is bound to.
Avoiding Problems
In general, avoid doing data processing on the main queue that is not user-related. Data processing can be CPU-intensive, and if it is performed on the main queue, it can result in unresponsiveness in the user interface. If your application will be processing data, like importing data into Core Data from JSON, create a private queue context and perform the import on the private context.
Do not pass NSManagedObject instances between queues. Doing so can result in corruption of the data and termination of the app. When it is necessary to hand off a managed object reference from one queue to another, use NSManagedObjectID instances.
You retrieve the managed object ID of a managed object by calling the objectID accessor on the NSManagedObject instance.
as per apple documentation
Concurrency is the ability to work with the data on more than one
queue at the same time. If you choose to use concurrency with Core
Data, you also need to consider the application environment. For the
most part, AppKit and UIKit are not thread-safe. In macOS in
particular, Cocoa bindings and controllers are not threadsafe—if you
are using these technologies, multithreading may be complex.
Using a Private Queue to Support Concurrency
In general, avoid doing data processing on the main queue that is not
user-related. Data processing can be CPU-intensive, and if it is
performed on the main queue, it can result in unresponsiveness in the
user interface. If your application will be processing data, such as
importing data into Core Data from JSON, create a private queue
context and perform the import on the private context.

Best practice manipulating Core Data Context on UI interaction while sync in background at the same time

I've already done research in lots of articles and discussions about how to use NSManagedObjectContext, but still could't find a satisfying architecture for my project.
In my app, there are three sources where the data might be modified from, ordered by their priorities when there is conflict simultaneously (ex. the cloud has the least priority):
User interface,
BLE message,
HTTP response from the cloud
Since I'm still not an expert on iOS development, I tried to avoid using multiple context for each source. However, after weeks of trial and error, I am reluctant but start to think if I really need to go for multi-context approach.
At the very beginning, I tried to use context.perform { } on the main context to do all the data change operations (add/update/delete, except fetch). I kept fetch to be a sync function because I want the data fetch to be instant so that can responsive with the UI. However, under this approach, I occasionally receive "Collection <__NSCFSet: 0x000000000> was mutated while being enumerated" exception (which I suppose might happen in the forEach or map function for batch data processing). I also found that this approach still block the UI when there are bunch of records to update in the background queue.
Therefore, I created a background context and use parent-child model to manipulate data. Basically the main context(parent) is only responsible for fetching data, while the background context(child) manipulates all the data change (add/update/delete) via backgroundContext.perform { }. This approach solves the UI block problem, however the collection mutated error still happen occasionally, and another issue occurs under this structure: for instance, the app would crash when I add a data record in ViewController A, and move to View Controller B, which fetch the same data immediately even if the background context haven't finished adding the data record.
Therefore I would like to ask some suggestions for the Core Data usage on my project. Is there something I did wrong under my parent-child data context model? Or, should I inevitably go for a real multi-context model without parent-child? How to do it?
My main context(parent) and background context(child) are initiated like this:
lazy var _context: NSManagedObjectContext = {
return (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
}()
lazy var _backgroundContext: NSManagedObjectContext = {
let ctx = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType)
ctx.parent = self._context
return ctx
}()
And the merge function is as follow:
#objc func contextDidSaveContext(notification: NSNotification) {
let sender = notification.object as! NSManagedObjectContext
if sender === self._context {
NSLog("******** Saved main Context in this thread")
self._backgroundContext.perform {
self._backgroundContext.mergeChanges(fromContextDidSave: notification as Notification)
}
} else if sender === self._backgroundContext {
NSLog("******** Saved background Context in this thread")
self._context.perform {
self._context.mergeChanges(fromContextDidSave: notification as Notification)
}
}
else {
NSLog("******** Saved Context in other thread")
self._context.perform {
self._context.mergeChanges(fromContextDidSave: notification as Notification)
}
self._backgroundContext.perform {
self._backgroundContext.mergeChanges(fromContextDidSave: notification as Notification)
}
}
}
Any advice of the core data structure would be appreciated.
Have you explored to use NSFetchedResultsController to fetch the data?
NSFetchedResultsController provides a thread safe way to observe for changes in the data due to Create, update or delete operations in CoreData.
Use NSFetchedResultsController's delegate methods to update the UI.
Ideally, controllerDidChangeContent delegate would give you an indication to update the UI. Documentation for reference

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.

UIDocument autosave not working when calling updateChangeCount

I have a custom subclass of UIDocument that I use to store the user's content for my app. I call -[UIDocument updateChangeCount:UIDocumentChangeDone] directly to track changes to the document. Saving and loading work fine, but the document never autosaves. Why would this be happening?
It turns out that the problem was that I wasn't calling -[UIDocument updateChangeCount:] from the main thread. Despite the fact that UIDocument isn't a UI element, it is still part of UIKit and so the usual caveats about always interacting with UIKit classes from the main thread still applies.
Wrapping the code in a dispatch to the main queue fixed the issue:
dispatch_async(dispatch_get_main_queue(), ^{
[doc updateChangeCount:UIDocumentChangeDone];
});
First, an update on Jayson's answer for Swift:
DispatchQueue.main.async {
doc.updateChangeCount(.done)
}
This works fine if you are just calling it in one or two places. However, if you have multiple calls, and a possibility of them being on background threads, then it may be beneficial to sub class UIDocument and override the updateChangeCount(:) function so that you enforce a main call. Otherwise, you become responsible for making the main call every time, which opens you up to a potential miss, leading to the document getting into the saveError state.
You would then have an override in your subclass like this:
override func updateChangeCount(_ change: UIDocumentChangeKind) {
DispatchQueue.main.async {
super.updateChangeCount(change)
}
}

Resources