I have a problem with a UITableView. I load data from a database server in a background thread and if it is finished it sends a notification. In the notification I update the data array of my view and use reloadData on the tableview.
Then the tableview deselects the selected row (that means the data is reloaded) and if I want to select another row I get EXC_BAD_ACCESS on the first line of didSelectRowAtIndexPath, even if it just is an NSLog.
If I don't assign the new array the background thread gives me to the variable data and don't use reloadData I have no problems with didSelectRowAtIndexPath but the tableview doesn't show the recent records. The user had to close the view and open it again to see the changes. That is really bad and I wanted to show the changes immediatly after the background thread finished to load the records from the server.
declared variables in the .h file:
-downloadThread is an NSThread,
-data is an NSArray,
-manager is my SQL interface.
-(void)viewDidLoad
{
...
[super viewDidLoad];
NSMutableArray *arr = [[manager getTeilnehmerList] retain];
data = arr;
[self.tableView reloadData];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(KundenUpdated:) name:#"ContactUpdate" object:nil]; // to be notified when updating thread is finished
downloadThread = [[NSThread alloc] initWithTarget:self selector:#selector(teilnehmerLoad:) object:nil]; //Thread to get actual data on Background
[downloadThread performSelector:#selector(start) withObject:nil afterDelay:2];
...
}
-(void)teilnehmerLoad:(id)sender
{
[manager loadTeilnehmerFromServerAndInsertIntoDatabase];
//data = [manager getTeilnehmerList];
[self.tableView reloadData];
[[NSNotificationCenter defaultCenter] postNotificationName:#"ContactUpdate" object:nil];
}
-(void)KundenUpdated:(NSNotification*)notifaction
{
#synchronized(self)
{
//needs function to select row that was selected before reload if the data record is still there after sync with server
[self.tableView reloadData];
NSLog(#"count data in kundenupdated: %i",data.count);
}
}
I suspect that teilnehmerLoad is called in your background thread, and it's calling reloadData, which is a no-no.
Change it (and/or KundenUpdated) to use performSelectorOnMainThread to do the reloadData.
Make sure you're not doing any other UI operations from your background thread.
Related
I have a tabBarView which have two tableViews. each of these tableViews will represent some news from a remote server. I want to populate tableView's datasource when tableViewController's init method is called. so I have put the needed networking operation inside init method. My init method is this:
- (instancetype) init{
self = [super init];
[NewsManager fetch:10 remoteNewsOfLanguage:#"fa" withOffsett:1 andCompletionHandler:^(NSMutableArray *news) {
self.newsList = news;
}];
self.tabBarItem.title = #"my title";
return self;
}
newsList is an array holding news loaded from server.
But when I run my project the order of invocation for tableViewController's methods is like the following:
tableViewController's init method is called and finished (but the completion handler block is not called yet)
tableViewController's viewDidLoad method is called ( it is called when the tableViewController is added to tabBarView's viewControllers array)
tableViewController's delegate method tableView:numberOfRowsInSection is called
the network operation's completionHandler block is called and the newsList array is set to the retrieved news from server
So my problem is that before my newsList array is populated the method tableView:numberOfRowsInSection is called and so my tableView is not filled with any news. How should I solve this issue?
Thanks
you should reload table data after you get data from server. then only your table will show updated data.
[NewsManager fetch:10 remoteNewsOfLanguage:#"fa" withOffsett:1 andCompletionHandler:^(NSMutableArray *news) {
self.newsList = news;
[yourTableview reloadData];//add this line
}];
The added line does the job and makes the new data to be loaded in the tableView but there is a small point that I think you should consider
[tableView reloadData]
will be executed in a thread other than mainThread and this will cause a 5 to 10 seconds delay for the data to be loaded on the tableView.
to prevent this you should somehow tell it to run the reloadData method on the main thread. this is done with the dispatch_async. So you should call [tableView reloadData] like this:
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
I have an app that load data using AFNetworking, parsing income JSON data and populate table. I use different class to manage JSON and to populate UITableView.
I need to correctly set number of rows. Problem is, i guess its method load too early, before we get any data from web. I tried following:
-(NSInteger)numberOfElements{
return [self.dataDictArray count];
}
And then:
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.handler numberOfElements];
}
When handler is an instance type of class, that deal with JSON and declare -(NSInteger)numberOfElements.
How can i set correct number of elements in such situation? Apparently tableView load before we got web data, that's kind of confusing.
One way to do this is:
In success block of AFNetworking get method fire a notification
#define NOTIFICATION_NAME #"LoadNotification"
[[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_NAME object:self];
In viewDidLoad method of UITableViewController subclass set the class as observer of notification
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(receivedLoadingNotification:) name:NOTIFICATION_NAME object:nil];
In 'receivedLoadingNotification' method set the delegate and datasourceDelegate
-(void)receivedLoadingNotification:(NSNotification *) notification
{
if ([[notification name] isEqualToString:NOTIFICATION_NAME])
{
[self.tableView setDelegate:self];
[self.tableView setDataSource:self];
}
}
So, the controller will only call the dataSource and delegate methods when JSON data is successfully loaded from AFNetworking.
Hope it helps.
Strange problem, and strangely only since yesterday. I have a fetched results controller in which you can reorder the rows without any problem. After a fresh start of the app, everything works fine and fast. However if you change from this tab bar to another tab bar and edit some random textfield (which isn't even linked to core data and doesn't trigger any save), the reordering is very slow. And I was able to narrow it down only to the save context. But now I have no clue where to look further. Any advice? Here's my reorder function:
- (void)tableView:(UITableView *)tableView
moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath
toIndexPath:(NSIndexPath *)destinationIndexPath;
{
if (self.activeField && [self.activeField isFirstResponder]){
[self.activeField resignFirstResponder];
}
self.suspendAutomaticTrackingOfChangesInManagedObjectContext = YES;
//Makes only a mutable copy of the array, but NOT the objects (references) within
NSMutableArray *fetchedResults = [[self.fetchedResultsController fetchedObjects] mutableCopy];
// Grab the item we're moving
NSManagedObject *resultToMove = [self.fetchedResultsController objectAtIndexPath:sourceIndexPath];
// Remove the object we're moving from the array.
[fetchedResults removeObject:resultToMove];
// Now re-insert it at the destination.
[fetchedResults insertObject:resultToMove atIndex:[destinationIndexPath row]];
// All of the objects are now in their correct order. Update each
// object's displayOrder field by iterating through the array.
int i = 1;
for (MainCategory *fetchedResult in fetchedResults)
{
fetchedResult.position = [NSNumber numberWithInt:i++];
}
// Save
TICK;
[((AppDelegate *)[[UIApplication sharedApplication] delegate]) saveContext];
TOCK;
self.suspendAutomaticTrackingOfChangesInManagedObjectContext = NO;
}
Yesterday I've only inserted these two methods (which cannot be the problem, because after commenting them so they don't trigger, the same problem remained):
-(void) registerForKeyboardNotificationsWithTabBarHeight:(CGFloat)tabbarHeight andHeaderHeight:(CGFloat)headerHeight andFooterHeight:(CGFloat)footerHeight andBottomSpacingToTextFields:(CGFloat)spacingToTextFields
{
self.tabbarHeight = tabbarHeight;
self.footerHeight = footerHeight;
self.headerHeight = headerHeight;
self.spacingToTextFields = spacingToTextFields;
// Register notification when the keyboard will be show
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
// Register notification when the keyboard will be hide
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
FetchedResultsController:
- (void)setupFetchedResultsController
{
self.managedObjectContext = ((AppDelegate *)[[UIApplication sharedApplication] delegate]).managedObjectContext;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"MainCategory"];
request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"position" ascending:YES]];
self.fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:request
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil cacheName:#"MainCategoryCache"];
}
And the view will appear is also very harmless..
- (void)viewWillAppear:(BOOL)animated
{
//Initialize database
[super viewWillAppear:animated];
[self displayBudget];
/*
This is only necessary since the sum of all categories is calculated and the FetchedResultsController
doesn't get this since it's only monitoring a MainCategory and the changes occured in the Subcategories */
[self.tableView reloadData];
}
I'm thankful for any clues what could be the problem :-(
I would not recommend multi-threading in this situation. The issue is that your saves are being done at the wrong time. Moving them to a background queue is not going to solve the issue. You need to change your save behavior.
Writing to disk is expensive. There is no shortcut around it. Even if you move your saves into another context (there is an Apple example on this) they are still doing to take time and potentially block your UI (you cannot do a new fetch during a save). You need to change your save behavior.
You do not need to save every time a user moves a row. That is wasteful. You want to queue up your saves so that you are doing more each save. In addition, you should be saving when the user expects there to be a delay. When you push or pop a view might be a good time for a save. When your app goes into the background is a great time for a save.
Saving when the user wants smooth UI response is bad and causes a bad user experience. Look for opportunities where the user is expecting something to take time. Posting something to a server? Save at the same time.
Your NSFetchedResultsController, etc. will read unsaved data from your main NSManagedObjectContext just as it will saved data. You are not gaining anything on the UI by saving frequently.
Update 1
Step one in performance problems is to run your application in Instruments.
Measure the speed and then see what is costing you time. Time Profiler is your friend.
Yes it will slow drawing your UI because you should't use saving and creating objects in main Queue;
read this https://developer.apple.com/library/ios/documentation/cocoa/conceptual/CoreData/Articles/cdConcurrency.html
you should have another background thread and you should do inserting deleting stuff there and then you have to pass NSObjectID_s for newManagedObject_s and then get it in main thread:
NSManagedObjectContext * mainThreadContext;
NSManagedObject * object = [mainThreadContext objectWithId:objectID];
I have a UITableView that sometimes has rapid insertions of new rows. The insertion of the new rows is handled by a notification observer listening for the update notification fired whenever the underlying data changes. I use a #synchronized block around all the data model changes and the actual notification post itself... hoping that each incremental data change (and row insertion) will be handled separately. However, there are times when this still fails. The exception will tell me that it expects 10 rows (based on the count from the data model), it previously had 8 rows, but the update notification only told it to insert a single row (as this is the first of two rapidly fired notifications).
I'm trying to understand how other people tend to handle these types of situations. How do other developers mitigate the problems of having multi-threaded race conditions between two table view update operations? Should I have a more secure lock that controls the update notifications (and why isn't #synchronized doing what it's supposed to)?
Any advice is greatly appreciated! Thanks.
Some pseudo-code:
My model class has a method like this, which gets called by other threads to append new rows to the table view:
- (void)addRow:(NSString *)data
{
#synchronized(self.arrayOfData)
{
NSInteger nextIndex = self.arrayData.count;
[self.arrayData addObject:data];
[NSNotificationCenter.defaultCenter postNotificationName:kDataUpdatedNotification object:self userInfo:#{#"insert": #[[NSIndexPath indexPathForRow:nextIndex inSection:0]]}];
}
}
My controller class has a method like this to accept the kDataUpdatedNotification notification and actually perform the row insertion:
- (void)onDataUpdatedNotification:(NSNotification *)notification
{
NSDictionary *changes = notification.userInfo;
[self.tableView insertRowsAtIndexPaths:changes[#"insert"] withRowAnimation:UITableViewRowAnimationBottom];
}
You're going to have this problem if you change your data model asynchronously with the main queue because your table view delegate methods are looking at the current state of the data model, which may be ahead of the inserts you've reported to the table view.
UPDATE
One solution is to queue your updates on a private queue and have that queue update your data model on the main queue synchronously (I have not tested this code):
#interface MyModelClass ()
#property (strong, nonatomic) dispatch_queue_t myDispatchQueue;
#end
#implementation MyModelClass
- (dispatch_queue_t)myDispatchQueue
{
if (_myDispatchQueue == nil) {
_myDispatchQueue = dispatch_queue_create("myDispatchQueue", NULL);
}
return _myDispatchQueue;
}
- (void)addRow:(NSString *)data
{
dispatch_async(self.myDispatchQueue, ^{
dispatch_sync(dispatch_get_main_queue(), ^{
NSInteger nextIndex = self.arrayData.count;
[self.arrayData addObject:data];
[NSNotificationCenter.defaultCenter postNotificationName:kDataUpdatedNotification object:self userInfo:#{#"insert": #[[NSIndexPath indexPathForRow:nextIndex inSection:0]]}];
});
});
}
The reason you need the intermediate dispatch queue is as follows. In the original solution (below), you get a series of blocks on the main queue that look something like this:
Add row N
Add row N+1
Block posted by table view for row N animation
Block posted by table view for row N+1 animation
In step (3), the animation block is out-of-sync with the table view because (2) happened first, which results in an exception (assertion failure, I think). So, by posting the add row blocks to the main queue synchronously from a private dispatch queue, you get something like the following:
Add row N
Block posted by table view for row N animation
Add row N+1
Block posted by table view for row N+1 animation
without holding up your worker queues.
ORIGINAL Solution still has issues with overlapping animations.
I think you'll be fine if you update your data model on the main queue:
- (void)addRow:(NSString *)data
{
dispatch_async(dispatch_get_main_queue(), ^{
NSInteger nextIndex = self.arrayData.count;
[self.arrayData addObject:data];
[NSNotificationCenter.defaultCenter postNotificationName:kDataUpdatedNotification object:self userInfo:#{#"insert": #[[NSIndexPath indexPathForRow:nextIndex inSection:0]]}];
});
}
Currently, I'm downloading data from a web server by calling a method fetchProducts. This is done in another separate thread. As I successfully download fifty items inside the method stated above, I post a notification to the [NSNotification defaultCenter] through the method call postNotificationName: object: which is being listened to by the Observer. Take note that this Observer is another ViewController with the selector updateProductsBeingDownloadedCount:. Now as the Observer gets the notification, I set the property of my progressView and a label that tells the progress. Below is the code I do this change in UI.
dispatch_async(dispatch_get_main_queue(), ^{
if ([notif.name isEqualToString:#"DownloadingProducts"]) {
[self.progressBar setProgress:self.progress animated:YES];
NSLog(#"SetupStore: progress bar value is %.0f", self.progressBar.progress);
self.progressLabel.text = [NSString stringWithFormat:#"Downloading %.0f%% done...", self.progress * 100];
NSLog(#"SetupStore: progress label value is %#", self.progressLabel.text);
[self.view reloadInputViews];
}
});
The idea is to move the progressView simultaneously as more items were being downloaded until it is finished. In my case, the progressView's animation will just start right after the items were already downloaded, hence a delay. Kindly enlighten me on this.