I'm having a threading issue loading images in a collectionview where the data is coming from cloudkit. I know this is a threading/blocking issue because before I implemented CK, I dumped some images in a folder on my desktop and read/parsed them from there and had no issue. With CK, I just created a handful of records via the dashboard and I'm successfully getting the expected records returned and use the images from those results to populate the CV cells. I store the CK query results in an array and use the size of that array to set the numberOfItemsInSection delegate.
Here's the issue...in the numberOfItemsInSection delegate method, I'm calling the model class, which executes the CK query. Since that is obviously a network call, I put that in a background thread. From logging, I can see the query execute and the results come back very quickly - within 2-3 seconds. However, the CV cells never display and I don't see the custom cell get initialized (via logging). But if I tap the camera button and take a photo, which I've implemented, I take the resulting image and add it to the array, then call reloadData on the CV and all the cells (and images) appear, including the new image just taken with the camera.
By accident, I found out a hack that somewhat works, which is calling reloadData on the CV inside the background thread of the numberOfItemsInSection delegate method. As a result, I thought I might have stumbled on to the solution by switching back to the main thread when calling reloadData, but that put it in a sort of endless loop of continuously calling the numberOfItemsInSection method and cellForItemAtIndexPath and made it to where it lagged to a point that you could barely scroll and tapping on any of the cells wouldn't do anything.
At this point, after trying many, many various things, I'm at a complete loss on how to fix this. I know this is probably a pretty easy solution as it's very common to load images asynchronously to populate a collectionview or tableview. Can someone please provide some guidance? Thanks in advance!!!
#property (nonatomic) NSInteger numberOfItemsInSection;
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
NSLog(#"***numberOfItemsInSection***");
dispatch_queue_t fetchQ = dispatch_queue_create("load image data", NULL);
dispatch_async(fetchQ, ^{
self.numberOfItemsInSection = [self.imageLoadManager.imageDataArray count];
[self.myCollectionView reloadData]; // should be done on main thread!
});
NSLog(#"numberOfItemsInSection: %ld", (long)self.numberOfItemsInSection);
return self.numberOfItemsInSection;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell"; // string value identifier for cell reuse
ImageViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
NSLog(#"cellForItemAtIndexPath: section:%ld row:%ld", (long)indexPath.section, (long)indexPath.row);
cell.layer.borderWidth = 1.0;
cell.layer.borderColor = [UIColor grayColor].CGColor;
cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
ImageData *imageData = [self.imageLoadManager imageDataForCell:indexPath.row]; // maps the model to the UI
dispatch_async(dispatch_get_main_queue(), ^{
if (imageData.imageURL.path) {
cell.imageView.image = [UIImage imageWithContentsOfFile:imageData.imageURL.path];
[cell setNeedsLayout];
} else {
// if imageURL is nil, then image is coming in from the camera as opposed to the cloud
cell.imageView.image = imageData.image;
[cell setNeedsLayout];
}
});
return cell;
}
before returning self.numberOfItemsInSection you should wait until the async call is finished. You can do that using semaphores. But then why are you doing this async? You are just getting the count of an array. And then you shouldn't reloadData there. Where do you start your CloudKit query? are you doing that onViewDidLoad? That is also an async operation. When that completes just doe a reloadData of your collectionView. Besides that doing this would be enough:
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return [self.imageLoadManager.imageDataArray count];
}
If you really want to use async there, then you do have to wait for the result. You could change your code to something like:
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
NSLog(#"***numberOfItemsInSection***");
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
dispatch_queue_t fetchQ = dispatch_queue_create("load image data", NULL);
dispatch_async(fetchQ, ^{
self.numberOfItemsInSection = [self.imageLoadManager.imageDataArray count];
[self.myCollectionView reloadData]; // should be done on main thread!
dispatch_semaphore_signal(sema);
});
NSLog(#"numberOfItemsInSection: %ld", (long)self.numberOfItemsInSection);
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
return self.numberOfItemsInSection;
}
And then why do you go to the main queue in cellForItemAtIndexPath? It's already executed on the main queue.
Related
I'm using AFNetworking and MagicalRecord (the current develop branch) and I'm trying to figure out how to import a lot of objects which are dependent on each other. Each resource/entity has multiple pages worth of downloads. I have a class managing the downloads for a given entity and saving them using MagicalDataImport (which has been amazing).
I believe my issue is that the imports aren't happening on the same thread. So I think what is happening is:
In one thread, EntityA is getting saved properly and propagated to the parent entity.
Then in another thread, EntityB is being saved, and along with it it's relationship to EntityA is built. That means a blank (fault?) object is being created. Then when it gets propagated to the parent entity, I believe EntityA is overwriting the EntityA that is there. Thus I'm left with some objects that don't have all of the attributes.
At least, I think that is what is happening. What I'm seeing via the UI is actually that the relationships between entities aren't always built correctly.
My end goal is to get the entire download/import process to be done in the background, not effecting the UI at all.
Here is my AFJSONRequest:
AFJSONRequestOperation *operation = [AFJSONRequestOperation
JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON)
{
[self saveResources:[JSON objectForKey:#"data"]];
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON)
{
DLog(#"%#",error.userInfo);
[self.webService command:self didFail:error.localizedDescription];
}];
[operation setQueuePriority:self.priority];
And it calls saveResources::
- (void)saveResources:(NSArray*)resources {
BOOL stopDownloads = [self stopDownloadsBasedOnDate:resources];
if ([resources count] > 0 && !stopDownloads){
self.offset = #([offset intValue] + [resources count]);
[self send];
}
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *blockLocalContext) {
[self.classRef MR_importFromArray:resources inContext:blockLocalContext];
} completion:^(BOOL success, NSError *error) {
if (error){
// ... handle errors
}
else {
// ... handle callbacks
}
}];
}
This kicks off another download ([self send]) and then saves the objects.
I know by default AFNetworking calls the callback in the main queue, and I've tried setting the SuccessCallbackQueue/FailureCallbackQueue to my background thread, but that doesn't seem to solve all the issues, I still have some relationships going to faulted objects, though I think I do need to do that to keep everything going in a background thread.
Is there anything else I need to call in order to properly propagate these changes to the main context? Or is there a different way I need to set this up in order to make sure that all the objects are saved correctly and the relationships are properly built?
Update
I've rewritten the issue to try to give more clarification to the issues.
Update
If you need more code I created a gist with (I believe) everything.
I ended up having this exact same issue a few days ago. My issue was I had received a customer record from my API with AFNetworking. That customer could have pets, but at this point I didn't have the petTypes to correspond to the customers pet record.
What I did to resolve this was create a transformable attribute with an NSArray which would temporarly store my pets until my petTypes were imported. Upon the importation of petTypes I then triggered an NSNotificationCenter postNotification (or you can just do the pet import in the completion).
I enumerated through the temporary transformable attribute that stored my pet records and then associated the with the petType
Also I see you are doing your import inside of a save handler. This is not needed. Doing your MR_importFromArray will save automatically. If you are not using an MR_import method then you would use the saveToPersistentStore methods.
One thing is I don't see where you are associating the relationships. Is EntityB's relationship to EntityA being sent over via JSON with the EntityA objecting being in EntityB?
If so then this is where the relationship is getting messed up as it is creating / overwriting the existing EntityA for the one provided in EntityB. My recommendation would be to do something like this.
NSArray *petFactors = [responseObject valueForKeyPath:#"details.items"];
NSManagedObjectContext *currentContext = [NSManagedObjectContext MR_context];
Pets *pet = [Pets MR_findFirstByAttribute:#"id" withValue:petId inContext:currentContext];
pet.petFactors = nil;
for (id factor in petFactors) {
[pet addPetFactorsObject:[PetFactors MR_findFirstByAttribute:#"id" withValue:[factor valueForKey:#"factorId"]]];
}
[currentContext MR_saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error) {
if (success) {
NSLog(#"SAVED PET FACTORS");
[[NSNotificationCenter defaultCenter] postNotificationName:kPetFactorsSavedSuccessfully object:nil];
} else {
NSLog(#"Error: %#", error);
}
}];
I'm putting this as an answer, though I'm not 100% sure if this is your issue or not. I think the issue stems from your localContext. Here is a sample web request method from an app we wrote that uses data importing, you may be able to use it as an example to get yours working.
Note that the AFNetworking performs its completion block on the main thread, then the MagicalRecord saveInBackground method switches back to a background thread to do the importing and processing, then the final MR completion block performs the handler block on the main thread again. The localContext that's used to import is created/managed by the saveInBackground method. Once that method is complete the context is saved and merged with the app's main context and all the data can then be accessed.
- (void)listWithCompletionHandler:(void (^)(BOOL success))handler{
[[MyAPIClient sharedClient] getPath:#"list.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject){
NSString *statusString = [responseObject objectForKey:#"status"];
// Handle an error response
if(![statusString isKindOfClass:[NSString class]] || ![statusString isEqualToString:#"success"]){
// Request failure
NSLog(#"List Request Error: %#", statusString);
NSLog(#"%#", [responseObject objectForKey:#"message"]);
if(handler)
handler(NO);
return;
}
NSArray *itemsArray = [responseObject objectForKey:#"items"];
[MagicalRecord saveInBackgroundWithBlock:^(NSManagedObjectContext *localContext){
// Load into internal database
NSArray *fetchedItems = [Item importFromArray:itemsArray inContext:localContext];
NSLog(#"Loaded %d Items", [fetchedItems count]);
} completion:^{
if(handler)
handler(YES);
}];
} failure:^(AFHTTPRequestOperation *operation, NSError *error){
NSLog(#"Fail: %#", error);
if(handler)
handler(NO);
}];
}
I have an entity called Practice and I use a View Controller called SelectorViewController to select one of the practices, selectedPractice. I then return selectedPractice to a view Controller called RegularViewController where I display some of the selectedPractice attributes. All of this works fine. However the app has a number of other View Controllers which can be reached by modal segues from instances of RegularViewController. As a result, if I leave and then come back to RegularViewController, selectedPractice is reset as null. I would also like to save selectedPractice so that it is available at app initialisation if it has previously been set in SelectorViewController. How do I achieve this by making selectedPractice persistent across the app, and available at runtime?
Regards
Thanks to the post above, which was great, I managed to sort it. Here is my code, which may be very clumsy, but it works.
Firstly, as I loaded the fetchedObjects into a PickerView in SelectorView Controller, I set an attribute "isSelectedPractice" to "NO" with the following code:
for (Practice *fetchedPractice in [self.fetchedResultsController fetchedObjects]) {
[fetchedPractice setValue:#"NO" forKey:#"isSelectedPractice"];
[self.managedObjectContext save:nil];
I then identified for the selected Practice:
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
Practice *practice = [[self.fetchedResultsController fetchedObjects] objectAtIndex:row];
self.selectedPractice = practice;
NSLog(#"The '%#' practice was selected using the picker", self.selectedPractice.name);
}
as the view Segue'd back to RegularViewController I set the isSelectedPractice attribute for selectedPractice to YES. I kept it this late as I didn't want more than one selection in the PickerView to result in multiple objects with isSelectedPractice YES.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"SavedPractice Segue"])
{
[self.selectedPractice setValue:#"YES" forKey:#"isSelectedPractice"];
[self.managedObjectContext save:nil];
NSLog(#"Setting SelectedPractice as '%#' in RegularViewController with isSelectedPractice as '%#'",self.selectedPractice.name,self.selectedPractice.isSelectedPractice );
RegularViewController *rvc= segue.destinationViewController;
rvc.delegate = self;
rvc.selectedPractice = self.selectedPractice;
}
else {
NSLog(#"Unidentified Segue Attempted!");
}
}
I then set the following Predicate in the setupFetchedResultsController method of RegularViewController:
request.predicate = [NSPredicate predicateWithFormat:#"isSelectedPractice = %#", #"YES"];
Many thanks for the help
Without seeing your actual project, one way I know will work but might be a little too round a bout would be to add an attribute "isSelectedPractice" to your entity. You could make it a BOOL, but I've had mixed results with BOOL's in Core Data, I prefer to just leave it as a NSString and set it to "yes" or "no". Then when you pull it down, modify it or add it to core Data as a entity with isSelectedPractice set to "yes". Then in your other controllers, do a
if (self.managedObjectContext == nil) {
self.managedObjectContext = [(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
}
then do a fetch request to get entities with a predicate which is looking for isSelectedPractice equaling "yes". If you need actual code samples on how to do this let me know and I'll edit them in.
I have a page that checks if there is a list of employees on the bundle, and if there is it displays them in a table view. But if there is no list on the bundle it throws up a modal view controller. That then requires someone to login, the login is authenticated and then data is downloaded.
The ModalView is setup to be a delegate of the first page. I can call the delegate method just fine and pass the list, but when the ModalView is dismissed the table does not reload the tableview with the data. If i run the project again it loads the list up in the table view instantly.
Here is the method on the ViewDidLoad of the first page
[self checkLastLoginDate];
[self loadDataIntoArray];
if (currentData && dataLoadedIntoArray) {
NSLog(#"sweet the data is current");
[self createIndexedArray];
}
else{
NSLog(#"Data not loaded and not current");
//push login view
///if ipad do this
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
//the above code was working fine. I am just testing pushing a dynamic xib
LoginViewControlleriPad *loginControlleriPad = [[LoginViewControlleriPad alloc] initWithNibName:#"LoginViewControlleripad" bundle:nil];
loginControlleriPad.modalPresentationStyle = UIModalPresentationFormSheet;
loginControlleriPad.delegate = self;
[self presentModalViewController:loginControlleriPad animated:YES];
[self createIndexedArray];
[loginControlleriPad dismissModalViewControllerAnimated:NO];
[self.tableView reloadData];
}
The first two methods check to make sure the data exists on the bundle and that the last login date is within 15 days.
if it is then I create an IndexedArray and it displays nicely.
If the data is not on the bundle or the date is to old I check the device and use a modal view. The delegates are set and the ModalView Appears.
In the ModalView I use a syncronus and asyncronus request to hit a server for the required information. Then I create the list and pass it back to the first page.
Once the connection is made and we do a little work on the list we save it to the bundle.
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *path = [documentsDirectory stringByAppendingPathComponent:kEMPLOYEE_PLIST];
/////write to file/////
[cleanResults writeToFile:path atomically:YES];
employeeList = [[NSMutableArray alloc ]initWithContentsOfFile:path];
if (employeeList != nil)
{
[delegate passUserInfo:employeeList];
//cant get login view to dismiss
//[self dismissModalViewControllerAnimated:YES];
}
Once I get the list I right it to file and then read it back in. If there is info in the list I call the delegate method.
UPDATE
Here is the delgate method
- (void)passUserInfo: (NSMutableArray *)employeeDataArray
{
employeeData = [[NSMutableArray alloc] initWithArray:employeeDataArray];
[self.tableView reloadData];
/////FIX////////
[createIndexedArray];
}
Once that is called nothing happens. I tried dismissing the modal view, which works, but then the first page does not refresh and my app gets lost in the oblivion. I am just confused at were it is going and how i can refresh the tableview on the first page.
Is there a step I are missing? I have tried tracking it and lose it after this step.
I actually figured out that my code was setup right, i just needed to call a method that sorted the array into an alphabetic array. Once i called that method from the delegate method it worked like a snake charm.
UPDATE
I had a method that created an indexed array. When i called that inside the delegate method it reloaded the page and inserted the new into the table view. I am pretty sure it was reloading the whole time, i was just not calling the method that was populating the array that was being being displayed in the tableview.
I'm working with Core Data for the first time, with the Stanford iOS app development course as a guide. I pretty much copied the code from the demo app (of course I adjusted it to my needs), but I'm having two problems currently.
My app is a map view which on the tap of a button presents a modal view controller. This modal view checks whether a UIManagedDocument was created. If not, it creates one and inserts the data. This data is coming from a property list (258 items, so nothing too excessive). If it was created already (by previously displaying that view), if my logic holds, it should be safe to assume it also has content because the NSManagedObjects are created at the same time a document is created. The first run works perfectly fine, the table loads and all my data is correctly displayed.
However, when I dismiss and then re-display my modal view, the table stays empty. I'm checking the document's state, which is UIDocumentStateNormal, so querying it should be fine. But it isn't: my fetchedResultsController returns 0 rows. If I understand UIManagedContext correctly, the behavior I'm experiencing could be caused by a wrong/different context, but I make sure that: 1) I pass my document (not just the context) to the modal view in prepareForSegue:sender, and 2) I pass my document with the context back to the presenting view when the modal view is being dismissed. That's why I think it's probably not the context, but something else.
One other thing: inserting the 258 records when the app is first launched is fast enough in the simulator. However, on my phone it could take a whole 13 seconds. The insertion code is shown below (modified for readability):
+ (Department *)departmentName:(NSString *)name
withAttributes:(NSDictionary *)attributes
inContext:(NSManagedObjectContext *)context {
Department *department = [NSEntityDescription insertNewObjectForEntityForName:#"Department" inManagedObjectContext:context];
department.name = name;
NSArray *informationElements = [attributes objectForKey:#"information"];
for (int i = 0; i < [informationElements count]; i++) {
NSString *informationValue = [[informationElements objectAtIndex:i] objectForKey:#"value"];
if ([[[informationElements objectAtIndex:i] objectForKey:#"description"] isEqualToString:#"phone"]) {
department.phone = informationValue;
} else if ([[[informationElements objectAtIndex:i] objectForKey:#"description"] isEqualToString:#"email"]) {
department.email = informationValue;
} else if ([[[informationElements objectAtIndex:i] objectForKey:#"description"] isEqualToString:#"web"]) {
department.website = informationValue;
}
}
return department;
}
To be clear: this code works just fine, but it's really slow. It's encapsulated in a method which is called exactly 258 times. informationElements has at most three elements, which means there are 258 * 3 = 774 loops maximum. In reality it's much less than that, but even if it were 774, that shouldn't take 13 seconds, right?
The snippet below shows the initialization of UIManagedDocument:
if (![[NSFileManager defaultManager] fileExistsAtPath:[self.database.fileURL path]]) {
[self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
[self setupFetchedResultsController];
[self fetchDepartmentsIntoDocument:self.database];
}];
} else if (self.database.documentState == UIDocumentStateClosed) {
[self.database openWithCompletionHandler:^(BOOL success) {
[self setupFetchedResultsController];
}];
} else if (self.database.documentState == UIDocumentStateNormal) {
[self setupFetchedResultsController];
}
fetchDepartmentsIntoDocument reads the property list and then runs a loop which calls departmentName:withAttributes:inContext for every property list item.
If anyone could provide me with some help, it will be much appreciated!
For the speed issue, I would look into using predicates; that should speed things up a great deal!
Predicates make it so that the context returns only the values needed based on whatever criteria you choose. The reason they are faster is because it does not have to convert each stored entity object into a managed object, rather it can pull straight from the property, which speeds up comparisons drastically.
When you're inserting Department objects into the context, are you saving for each object? Inserting is relatively cheap, but saving (i.e. -[NSManagedobjectContext save:]) is expensive (since the database has to do locking and file I/O).
Also, on a more stylistic note, you can do
for (NSDictionary *element in informationElements) {
NSString *informationValue = [element objectForKey:#"value"];
if ([[element objectForKey:#"description"] isEqualToString:#"phone"]) {
department.phone = informationValue;
} else if ([[element objectForKey:#"description"] isEqualToString:#"email"]) {
department.email = informationValue;
} else if ([[element objectForKey:#"description"] isEqualToString:#"web"]) {
department.website = informationValue;
}
}
to iterate through your array of dictionaries.
I'm launching a localization request using Grand Central Dispatch :
- (void) findGroceriesNearMe {
dispatch_queue_t downloadQueue = dispatch_queue_create("Groceries downloader", NULL);
dispatch_async(downloadQueue, ^{
CLLocationCoordinate2D userLocation = [LocationManagerController findMeWithCaller:self];
dispatch_async(dispatch_get_main_queue(), ^{
[self userSuccessFullyFound:userLocation];
});
});
dispatch_release(downloadQueue);
}
It calls a static method in my Singleton class LocationManager Controller :
+ (CLLocationCoordinate2D) findMeWithCaller: (UIViewController *) viewController {
LocationManagerController *locationManagerController = [LocationManagerController locationManagerController];
[locationManagerController startUpdates];
while(![locationManagerController getterDone]){
//mystique pour nous-- a approfondir
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
In the startUpdates method, the CLLocationManager, property of LocationManagerController, is initialized and asked to startUpdatingLocation.
Finally, the method when location updates happen :
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
{
locationDenied = NO;
NSLog(#"%f,%f",newLocation.coordinate.latitude,newLocation.coordinate.longitude);
NSDate* eventDate = newLocation.timestamp;
NSTimeInterval howRecent = [eventDate timeIntervalSinceNow];
// On vérifie que la newLocation est récente
if (abs(howRecent) > 10.0) {
return;
}
// Test if it's not an invalid measurement
if (newLocation.horizontalAccuracy < 0) return;
// Test the measurement to see if it meets the desired accuracy
if (newLocation.horizontalAccuracy <= manager.desiredAccuracy)
{
latitude = newLocation.coordinate.latitude;
longitude = newLocation.coordinate.longitude;
locationDefined = YES;
[self setterDone:YES];
}
}
My problem is that the locationManager only send 3 location updates and then stops sending updates even though I didn't ask it to stop. So basically, I never get out of the while(![locationManagerController getterDone]) loop.
By the way, before trying to implement this using GCD, it was working fine so I guess the issue has to do with my implementation of multi-threading.
Any idea ?
Edit
I don't get any error in the console. The program just keeps running but I'm stuck in that while loop and nothing else happens after the 3 first location updates.
Thanks !
From CLLocationManager class reference:
Configuration of your location manager object must always occur on a
thread with an active run loop, such as your application’s main
thread.
A guess. If you are sitting at your desk and testing with your simulator the accuracy may not get better what you want
if (newLocation.horizontalAccuracy <= manager.desiredAccuracy)
So you may get stuck in your loop. Try with higher accuracy while at your desk. Also consider if the accuracy is never better that what you want since it maybe that the gps reception is not good.
Let me know if that helps or if I was way off the mark :-)
-- Fasttouch