Background Fetch and asynchronous UICollectionView - ios

I'm trying to implement the iOS Background Fetch API in an app. The app downloads JSON from a server, calls -[UICollectionView reloadData], and for every cell, and image is downloaded asynchronously in -collectionView:cellForItemAtIndexPath:.
In my initial implementation, I would call the completion handler passed by the system into application:performFetchWithCompletionHandler after I called reloadData. The app snapshot in the multitasking view would then display empty cells, because the images wouldn't have been downloaded yet. To solve that, I removed the completion handler call after reloadData, wrote a little structure keeping track of which cells' images have been downloaded, and only after a certain number have been downloaded, I would call the completion handler.
I did this using a completionBlock property on the view controller that reloads the images. The app delegate sets that property, then calls a reload method on that view controller, which then calls its completion handler property. I looks like this:
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
UINavigationController *navController = (UINavigationController *)self.window.rootViewController;
WSViewController *viewController = (WSViewController *)navController.topViewController;
viewController.completionBlock = ^(BOOL success, BOOL newData) {
if (!success) {
completionHandler(UIBackgroundFetchResultFailed);
} else if (success) {
if (newData) {
completionHandler(UIBackgroundFetchResultNewData);
} else {
completionHandler(UIBackgroundFetchResultNoData);
}
}
};
[viewController reload];
}
During testing, I found that performing a background fetch in relatively rapid succession would cause the completion handler not to be called. That's easily explained, because the completionBlock property is overwritten and the old one won't get called.
So, as advised in WWDC 2013 Session 204 "What's New With Multitasking", I removed the property and decided to pass the completion handler all the way through my code. -reload is now -reloadWithCompletionBlock: etc.
But now I'm stuck on how to implement that in the App Delegate. The View Controller has a delegate, and one of its methods, - (void)didFinishDownloadingImages, is implemented by the App Delegate. That's the point where I want to call the completion handler. But I can't, since there is no way to get to the completion handler without storing it in a property, defeating why I was doing it this way in the first place.
Any thoughts on how to solve this?

use NSOperationQueue and move your blocks of code into a subclass of NSOperation -- this will help you handle situations like blocking, discarding multiple requests and otherwise handling all those types of situations.
So, rather than write inline blocks, I'd move to an operation queue which seems like some lifting, but actually makes this much easier to handle properly.

Related

Is rootViewController always ready to present a segue by the time application:didBecomeActive is called (iOS)?

My app has a rootViewController setup in a storyboard. I originally asked this question, but I can see that it is the wrong question to ask now:
Is the viewDidLoad method of the rootViewController always called
before application:didBecomeActive in the app delegate? Does that
viewDidLoad method always finish before
application:didBecomeActive is called, or can the two run
concurrently?
I can run tests, but I know that these tests don't take into account
every situation. I want to understand if there are hard and fast rules
that apply here, or if I might possibly be dealing with race
conditions at some point. From what I can tell, viewDidLoad of the
rootViewController seems to be called and finished before
application:didbecomeActive is called, but I can't figure out if
this is something I can rely on.
I am using iOS 11 and Xcode 9.
My rootViewController serves to show my appUpdating view, passcode view, my legal terms view, my handleImportedFile view, and my tabbarcontroller. It is like a launch coordinator of sorts, because my loading process is extremely complicated. Now I am moving the location of the user's sqlite database, and some of my user's databases are huge. It has to be done in the background, but while a hud is running on the main queue, because the app cannot show data until this is done. If I do this during application:didFinishLaunchingWithOptions, some users are getting the watchdog timer. So I want to move the loading process to application:didBecomeActive (I have a flag to tell me if app is launching from terminated). When it is done running, the rootViewController performs a segue to the pertinent view. But I think rootViewController needs to have loaded by that time, and I am not sure that that is always the case.
Basically, in the code below, I am trying to figure out if [self.masterController showApplicableView], which causes the rootViewController (which I call masterController) to perform a segue, is called in a 'safe place', or if, in any case, it could possibly be too early to have the rootViewController perform a segue at this point.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.masterController = (MasterViewController*)self.window.rootViewController;
self.activatingFromTerminated = YES;
return YES;
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
self.activatingFromTerminated = NO;
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
if (_activatingFromTerminated) {
self.activatingFromTerminated = NO;
[[CoreDataController sharedManager] loadStoreWithCompletionHandler:^(NSError * _Nullable error) {
if ([CoreDataController sharedManager].storeLoaded) {
[self.masterController showApplicableView];//performs applicable segue
}
}];
}
}
From what I can tell, viewDidLoad of the rootViewController seems to be called and finished before application:didbecomeActive is called, but I can't figure out if this is something I can rely on.
It isn’t and you shouldn’t. You shouldn’t be concerned about this at all. The fact that you think you need to know this is a Bad Smell in your code. These are lifetime events from two completely different areas (the app and a view controller) and their relative timing should be no concern of yours. Do in each what is appropriate to the meaning of that event.

UITableView reloadData crashes on reappearance in iOS 11

Update: In my view, the question is still relevant and so I am marking a potential design flaw that I had in my code. I was calling the asynchronous data population method in viewWillAppear: of VC1 which is NEVER a good place to populate data and to reload a table view unless everything is serialized in the main thread. There are always potential execution points in your code when you must reload you table view and viewWillAppear is not one of them. I was always reloading table view data source in VC1 viewWillAppear when returning from VC2. But an ideal design could have used an unwind segue from VC2 and repopulate the data source upon its preparation (prepareForSegue) right from VC2, only when it was actually required. Unfortunately, it seems like nobody had mentioned it so far :(
I think there are similar questions that have been asked previously. Unfortunately none of them essentially addressed the issue I'm facing.
My problem structure is very simple. I have two view controllers, say VC1 and VC2. In VC1 I show a list of some items in a UITableView, loaded from the database and in VC2 I show the details of the chosen item and let it be edited and saved. And when user returns to VC1 from VC2 I must repopulate the datasource and reload the table. Both VC1 and VC2 are embedded in a UINavigationController.
Sounds very trivial and indeed it is, till I do everything in the UI thread. The problem is loading the list in VC1 is somewhat time consuming. So I have to delegate the heavy-lifting of data loading task to some background worker thread and reload the table on main thread only when data load completes to give a smooth UI experience. So my initial construct was something similar to the following:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
dispatch_async(self.application.commonWorkerQueue, ^{
[self populateData]; //populate datasource
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData]; //reload table view
});
});
}
This was very much functional until iOS10 from when UITableView stopped immediate rendering through reloadData and started to treat reloadData just as a registration request to reload the UITableView in some subsequent iteration of the run-loop. So I found that my app started to occasionally crash if [self.tableView reloadData] hadn't completed before a subsequent call to [self populateData] and that was very obvious since [self populateData] isn't thread-safe anymore and if datasource changes before the completion of reloadData it is very likely to crash the app. So I tried adding a semaphore to make [self populateData] thread-safe and I found that it was working great. My subsequent construct was something similar to the following:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
dispatch_async(self.application.commonWorkerQueue, ^{
[self populateData]; //populate datasource
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData]; //reload table view
dispatch_async(dispatch_get_main_queue(), ^{
dispatch_semaphore_signal(self.datasourceSyncSemaphore); //let the app know that it is free to repopulate datasource again
});
});
dispatch_semaphore_wait(self.datasourceSyncSemaphore, DISPATCH_TIME_FOREVER); //wait on a semaphore so that datasource repopulation is blocked until tableView reloading completes
});
}
Unfortunately, this construct also broke since iOS11 when I scroll down through UITableView in VC1, select an item that brings up VC2 and then come back to VC1. It again calls viewWillAppear: of VC1 that in turn tries to repopulate the datasource through [self populateData]. But the crashed stack-trace shows that the UITableView had already started to recreate its cells from scratch and calling tableView:cellForRowAtIndexPath: method for some reason, even before viewWillAppear:, where my datasource is being repopulated in background and it is in some inconsistent state. Eventually the application crashes. And most surprisingly this is happening only when I had selected a bottom row that was not on screen, initially. Following is the stack-trace during the crash:
I know everything would run fine if I call both the methods from the main thread, like this:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self populateData]; //populate datasource
[self.tableView reloadData]; //reload table view
}
But that is not something that is expected for a good user experience.
I feel the issue happens since UITableView is trying to fetch the offscreen top rows on reappearance, when scrolled down. But unfortunately after understanding so many damn things I could hardly sort it out.
I would really like the experts of this site to help me out of the situation or show me some way around. Thanks a loads in advance!
PS: self.application.commonWorkerQueue is serial dispatch queue running in the background in this context.
You should split your populateData function. Lets say for example into fetchDatabaseRows and populateDataWithRows. The fetchDatabaseRows should retrieve the rows into memory in its own thread and a new data structure. When the IO part is done, then you should call populateDataWithRows (and then reloadData) in the UI thread. populateDataWithRows should modify the collections used by the TableView.
UIKit runs on main thread. All UI updates must be on main thread only. There is no race condition if updates to data source happens on main thread only.
Important to understand is that you need to protect data. So if you are using semaphore or mutex or anything like this construct is always:
claim the resource for me. (ex: mutex.lock())
do the processing
unlock the resource (ex: mutex.unlock())
Thing is, that because UI thread is for UI and background thread is used for processing you can not lock shared data source, because you would lock UI thread as well. Main thread would wait for unlock from background thread. So this construct is big NO-NO. That means your populateData() function must create copy of data in the background while UI is using its own copy on main thread. When data are ready, just move the update into main thread (no need for semaphore or mutex)
dispatch_async(dispatch_get_main_queue(), ^{
//update datasource for table view here
//call reload data
});
Another thing:
viewWillAppear is not the place to do this update. Because you have navigation where you push your detail, you may do the swipe to dismiss, and in the midle just change your mind and stay in detail. However, vc1 viewWillAppear will be called. Apple should rename that method to "viewWillAppearMaybe" :). So right thing to do is to create a protocol, define method that will be called and use delegation to call the update function just once. This will not cause crash bug, but why to call update more than once? Also, why you are fetching all items, if only one has changed? I would update just 1 item.
One more:
You are probably creating reference cycle. Be careful when using self in blocks.
Your first example would be almost good if it looked like this:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
dispatch_async(self.application.commonWorkerQueue, ^{
NSArray* newData = [self populateData]; //this creates new array, don't touch tableView data source here!
dispatch_async(dispatch_get_main_queue(), ^{
self.tableItems = newData; //replace old array with new array
[self.tableView reloadData]; //reload
});
});
}
(self.tableItems is NSArray, simple data source for tableView as an example of data source)
My assumption is that because you have referee cycle when accessing self.tableView inside getMain. Ans there is leaked versions of this table view somewhere in background which started to crash app in iOS 11
There is a chance that you can verify this with memory graph in Xcode.
To fix this access you need access weak copy of self like this
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
__weak typeof(self) weakSelf = self;
dispatch_async(self.application.commonWorkerQueue, ^{
if (!weakSelf) { return; }
[weakSelf populateData]; //populate datasource
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.tableView reloadData]; //reload table view
});
});
}
In iOS11, the proper way to do "the heavy-lifting of data loading task" is to implement the UITableViewDataSourcePrefetching protocol as described here: https://developer.apple.com/documentation/uikit/uitableviewdatasourceprefetching
If you properly implement 'tableView:prefetchRowsAtIndexPaths:', you don't have to worry about background threads, worker queues, temporary datasources, or thread synchronization. UIKit takes care of all of that for you.
Update: after looking at your question a bit more thoroughly, it seems like the root cause of your problem is using a mutable backing data structure for your tableview. The system expects that the data will never change without an explicit call to reloadData in the same run loop iteration as the data change. The rows have always been loaded lazily.
As other folks have said, the solution is to use a readwrite property with an immutable value. When the data processing completes, update the property and call reloadData, both on the main queue.

Where is the correct place to populate UITableView?

I have a UITableView in my ViewController. To populate it, I have to make an async request that may take up to a second to complete. Where should I put it?
When I tried to make ViewDidLoad async and make a call from there, ViewWillLayoutSubviews throws an error, because it tries to position some elements that weren't assigned yet - due to the fact that not all code from ViewDidLoad was executed yet (I think).
Before awaiting anything in ViewDidLoad you need to setup all your view logic. Otherwise your view initialization will not be finished when ViewDidLoad method returns. That could be a potential cause for ViewWillLayoutSubviews to fail. If it still fails, use a try/catch to make sure your service is working:
public override async void ViewDidLoad()
{
base.ViewDidLoad();
// setup all the view elements here (before the async call)
try
{
var results = await MakeYourAsyncRequest();
InvokeOnMainThread(() =>
{
_tableView.Source = ...; // do something with the results
_tableView.ReloadData();
});
}
catch(Exception ex)
{
// do something with the exception
}
}
Try putting the tableView.ReloadData(); method inside
dispatch_async(dispatch_get_main_queue(), ^{} this might solve your issue.
-:As a general rule, you should try to make sure that all of your UI interaction happens on the main thread. And your data fetching task will work in background. It looked like you were calling reload Data from your
background thread, which seems risky.
Depending on the data I would put the call in the AppDelegate. When the app launches the data should be fetched and saved.
When your UITableview appears it will already have the data ready or maybe an error message since you already know the result of the fetch.
The data may change thats why I would also put the fetch call in viewWillAppear() of your ViewController with the UITableview.
ViewDidLoad() is a method that gets called only once. Also it is called as the first method of the ViewController lifecycle.
It would be good if you read a bit about it VC lifecycle.
You can help yourself by trying it in code with printf("method name").

Should I create one NSOperationQueue per UIViewController?

In almost every UIViewController I have a bunch of AFHTTPRequestOperations and I have to properly handle any kind of cancel (pressing cancel button, going back in UINavigationController's stack, etc.). I was wondering if creating one NSOperationQueue per each UIViewController and adding to it all operations called within controller would be a proper way to go? I was aiming for cancelling all operations [[NSOperationQueue mainQueue] cancelAllOperations] but this will kill all operations already started, especially those called from previous UIViewController. Or should I create property for each operation, call it in viewWillDissappear:(BOOL)animated and set if statement for cancel state in success block?
AFHTTPRequestOperationManager instances are cheap to create and each has its own operation queue, so it is easy to cancel all of a given UIViewController's operations:
- (void)dealloc {
[self.requestOperationManager.operationQueue cancelAllOperations];
}
This will cancel any request created through self.requestOperationManager. You can create the AFHTTPRequestOperationManager in your UIViewController's init method.
I recommend cancelling operations in your view controller's dealloc method, as you know it will no longer be needed.
Tried David Caunt answer, but didn't work for me. dealloc wasn't called when I pushed back button, so I guess the best way to cancel operations is to call it in viewWillDisappear: (my bad was to use && not || - silly mistake, but easiest things are often hardest to find)
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (self.isMovingFromParentViewController && self.isBeingDismissed){
DDLogDebug(#"back or dismissed!");
[self.manager.operationQueue cancelAllOperations]; }
}

Is there a required context for presentViewController?

My iOS app has a welcome screen (not to be confused with the default view). The view controller downloads and parses an XML file using NSXMLParser. When it completes it's task it makes a button visible, which when clicked calls "presentViewController" which takes the user into the actual app. This worked fine.
I then decided that I would just like the the app to automatically transition, and so I removed the button altogether and moved the call to presentViewController into the "parserDidEndDocument" delegate method. The method gets called but nothing happens. I suspect it has something to do with the context, but when I log "self" it prints an instance of the welcome view controller. What am I doing wrong? How should I fix this?
Try dispatching it to the main thread. Async objects like NSXmlParser work on separate threads, but UIKit updates must be done on the main thread.
dispatch_async(dispatch_get_main_queue(), ^{
[self presentViewController]; //Or whatever
});

Resources