Updating UI in when consuming RESTful API - ios

I have UI that uses scrollview with paging as well UITableView in one view controller. In order to load UI I am using an array of NSDictionaries (sourceArray) which has necessary information to draw UI. This array is created by consuming RESTful API that returns an array of records. I am returning that array in completion handler. Later I add that array along with some other key:value pairs to create NSDictioary which is later added to sourceArray for UI drawing.
Now my question is how can I update my UI based on changes in sourceArray. I am calling [self.view layoutIfNeeded]; but it does not force redraw.
- (void)getStateDetails {
NSArray *statesArray = [self getStates];
for (NSString *state in statesArray) {
[self getStateDetailsInState:state withCompletionBlock:^(NSArray *records) {
NSLog(#"%#",records);
NSDictionary *dict = #{
#"kImage": state,
#"kName": state,
#"kRecord": records
};
[self.sourceArray addObject:dict];
dispatch_async(dispatch_get_main_queue(), ^{
[self.view layoutIfNeeded];
});
}];
}
}
UI Details-
Viewcontroller contains UIScrollView with various UIViews which used dynamic data and a UITableView that contains dynamic data as well.
This is how I my current UI looks like.

The call to layoutIfNeeded will trigger your views to check to see if they need to update their layouts. Changing the contents of an array will not cause your view's layouts to need updating, so the call to layoutIfNeeded is unlikely to do anything.
In order for us to help you use the dictionary you load to update your UI, you're going to have to tell us what changes you expect the new array element to have on your UI.
One common thing to do is to display the contents of an array in a table view or collectionView. In that case you'd call the table/collection view's reloadData() function, which will cause it to ask it's data source for it's number of entries and re-render all of it's cells.
EDIT
Ok, and what is the relationship between the data you download and your UI? What code have you written that is supposed to cause the UI to update? You've changed your array, but you don't show any code that tells your view objects about updated data.
For a table view, that might be as simple as calling reloadData as mentioned above. For other views, how is your data being displayed into those views? What mechanism is supposed to install new data into those views?

The easiest way to get a view to re-draw itself is to call [UIView setNeedsDisplay] or by using [UIView setNeedsDisplayInRect]. This method tells the view that it needs to re-draw itself and it's subviews. However you shouldn't always have to call this. There are other methods that can cause views to redraw themselves. For instance if you add or remove a view to the view hierarchy using [UIView addSubview:] or [UIView removeFromSuperview].
These methods cause the system to call your view's drawInRect: method (assuming you overrode it with custom drawing code). If you did not override this method for custom drawing then it should cause your subviews to receive the same message so that they can get a chance to redraw.
Additional information here. Specifically read the section labeled "The View Drawing Cycle".
http://developer.apple.com/library/ios/#documentation/uikit/reference/uiview_class/uiview/uiview.html
One of the stackoverflow posts where you can add an observer and force call the controller to relaod
How to reload UIViewController

You have to use [tableView reloadData] and then if needed
[self.view layoutSubviews];
[self.view setNeedsDisplay];
[self.view setNeedsLayout];

Related

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 to add UI objects when programmatically creating UI (instead of .xib/storyboard)

In the UIViewControllers where I don't use .xib files, I've been creating my UI elements in the viewDidLoad methods. E.g.,
- (void)viewDidLoad {
[super viewDidLoad];
// Setup table
self.tableView=[[UITableView alloc] initWithFrame:self.view.frame];
[self.tableView setDataSource:self];
[self.tableView setDelegate:self];
[self.view addSubview:self.tableView];
// Setup custom cells
UINib *lessonNib=[UINib nibWithNibName:#"CustomCell" bundle:nil];
[[self tableView] registerNib:lessonNib forCellReuseIdentifier:#"CustomCellID"];
}
For the most part, this is working fine. But, in the process of investigating a bug associated with dynamic cell heights, I'm curious: is this the appropriate spot to add my UI elements?
Thanks for reading.
I would say it is the right place, yes. Since this method is only called once for sure and is called before anything of your view is shown on screen. And therefore you don´t risk creating them twice or even more often or too late.
But your layout should be done at a different place - in viewWillLayoutSubviews:
When a view's bounds change, the view adjusts the position of its subviews. Your view controller can override this method to make changes before the view lays out its subviews. The default implementation of this method does nothing.
You could also overwrite the corresponding viewDidLayoutSubviews
Regarding your comment: yes, layout information is not yet present in viewDidLoad, self.view.frame for example is not guaranteed to be the actual frame that your view will be displayed in later on. Further more a frame change by some part of your code would cause your UI to not respond if you set their size and position only on load.
Note: Setting up your subviews via code is far more tedious than just designing them in a storyboard - I would heavily recommend that if you don´t have serious concerns against using them.

UITableView cellForRowAtIndexPath gets called to early

I have a UITableView that loads its array items fine on the first call. However when i refresh the tableview it crashes because the delegate method cellForRowAtIndexPath gets called to early. I have an integer which represents the index of my main data array and this gets reset to 0 in my refresh button. However its crashing because its trying to reload the data before its been reset. I would normally use indexPath.row as my index however the array is complicated and the indexPath.row will not match up to what i want to show for each cell. Heres some code, any help is appreciated.
This gets called when i pull down to refresh AND in viewDidLoad to prepare the data
- (IBAction)refresh:(id)sender {
itemIndexer = 0;
[sender beginRefreshing];
[self loadData];
}
Part of my loadData method
-(void) loadData {
dispatch_queue_t backgroundQueue = dispatch_queue_create("com.Foo.myqueue", 0);
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(backgroundQueue, ^{
[self downloadData];
dispatch_async(mainQueue, ^{
[self.tableView reloadData];
[_rcRefresh endRefreshing];
});
});
In viewDidLoad i call to initially load the tableview:
[self refresh:_rcRefresh];
I am getting a index outside bounds of array error. Which i have used breakpoints to determine why, the reason is simply because the refresh isn't getting called at all otherwise itemIndexer would be set to 0. Instead its a number 1 greater than the array size. If its really necessary i can post cellForRowAtIndexPath however I'm unsure if you need it if it works on the first call. So to summarise the first call to the tableview works and the data is loaded fine however the refresh causes the index to be outside the bounds of the array as if refresh: is never called.
For what you say, I can only try guessing:
Your numberOfSectionsInTableView/numberOfRowsInSection is returning a wrong number.
If [self downloadData] is asynchronous (you are making a server request and not waiting for the response), you should reloadData once you have the data.
The data you download is not merged properly with the data you already have.
Some more code (numberOfSectionsInTableView, numberOfRowsInSection, cellForRowAtIndexPath, downloadData) would definitely help.
Couple of points for clarity...
As indicated by #k20, it is not that tableView:cellForRowAtIndexPath: is called too early, but that you need to better manage your download data once your asynchronous process / method has completed.
tableView:cellForRowAtIndexPath: is a UITableView data source method, not a delegate method. It is worth mentioning this pedantic detail because it may help you or others better understand the code you are writing. ;)
The table view method calls are what they are - as I understand it, all table view methods are called in order, each time a UITableViewController is init or awakeFromNib. Those that you must override (tableView:numberOfRowsInSection: & tableView:cellForRowAtIndexPath:), and those that you choose to override will still execute in that same order.
Therefore a more appropriate title for your question might be... "How to update a UITableView with data from a download on an asynchronous thread."
Again #k20 is pointing you to the correct solution. Have you attempting placing these two lines of code...
[self.tableView reloadData];
[_rcRefresh endRefreshing];
within your async call, instead of back in the main queue?
It may be that your code as written is executing like this...
Prepare local variables for dispatch_q_t;
Commence download process;
reload data for table view;
end the refresh [_rcRefresh endRefreshing];
depending on time it takes, then finish download process;
Where you obviously would like to execute like this...
Prepare local variables for dispatch_q_t;
Commence download process;
depending on time it takes, finish download process;
reload data for table view;
end the refresh [_rcRefresh endRefreshing];
Try my suggestion and let me know how you go. Hope that helps.

How can I clear all data in a view and reload it?

I have game that is played on a single view, when the game is over I want the user to be able to press a button (play again) that will completely reload the view (clearing all game data and refreshing the view as if it were loading for the first time). I have tried
[self.view setNeedsDisplay]
however nothing is happening. Do I have to manually clear out the data or is there a way to reset everything at once?
What I've similarly done in this case is to create a property which holds all of your game subviews, etc.
#property (nonatomic, strong) UIView *gameView;
Then in viewDidLoad, we call a method that sets up our game view for the first time (You will see we will use the same method a little later again for resetting the game)
- (void)viewDidLoad
{
[super viewDidLoad];
[self setUpGame];
}
- (void)setUpGame
{
// Your game views (subviews, buttons, etc.) set up here
self.gameView = [[UIView alloc] initWithFrame:self.view.bounds];
}
Then, when the user taps the play again button, we simply remove the game view from the superview, and call the prior method we discussed, which sets up the game again. Your button's target should be set to call this method below: [self playAgain];
- (void)playAgain
{
[self.gameView removeFromSuperview];
// This is the method we previously discussed above
[self setUpGame];
}
It's up to you to think of some cool and unique animations to make it a pleasing resetting of the game at this point :)
setNeedsDisplay just indicates that you would like iOS to redraw the screen, which you rarely should need to call manually.
I would probably implement something like #troop231 already stated, which is a reset method, but I would not re-allocate any buttons / views etc because that could be costly. In the MVC model, you should have your data (scores, # of lives, etc.) stored separately and your views should just reference them. So, reset the model and assuming you have your views aware of model changes, they will update accordingly.
Ways to do this include KVO, NSNotification, Core Data's NSFetchedResultsController (probably overkill), delegation, etc.

Saving NSManagedObjectContext casuing UITableView cells to disappear

I'm having the following issue.
I'm writing a RSS reader using CoreData and Apple Recipes example as a guide. I have a refresh button that re-downloads the RSS and verify using NSFetchRequest if there is new data. Once I finish going over the elements I commit the changes via the NSManagedObjectContext save method.
After the context save, the tableView disappears!
I then decided to call reloadData on the tableView to reflect the changes. So, once the NSManagedObjectContext is saved I call:
[self performSelectorOnMainThread:#selector(updateTableItems) withObject:nil waitUntilDone:NO];
-(void) updateTableItems {
[self.tableView reloadData];
}
This action causes the cell to delete the data while scrolling, when I pop the view and go back, I see all the changes and everything is okay.
I also read in one of the threads that I should use NSFetchedResultsController and make sure the UITableView is the delegate, same issue as the previous one.
What am I doing wrong ?
Why can't I see the changes in place?
Why the cell's content is being deleted?
Thanks!
It sounds like you are using two or more context on separate threads. You commit the save on the background thread context but don't merge the changes with the context connected to the UI on the front thread. This causes the UI context to come out of sync with the store which cause table rows to disappear when you scroll.
When you pop the controller by leaving the view, the context is deallocated such that when you go back to the view a second time, you have a new context aware of the changes to the store.
To prevent this problem, call refreshObject:mergeChanges: on the front context immediately after you save the background context. Have the front context register for a NSManagedObjectContextDidSaveNotification from the background context
I have been having a similar issue and it was driving me crazy. Basically in my code there are loads of competing threads trying to update the same data at the same time (the data behind the table view) and I think this some how causes the UITableView to "blow up" and its delegate methods stop firing. (You can prove this by adding NSLog's into the delegate methods).
It happens totally randomly and is really difficult to replicate.
I tried all sorts to fix this but the only thing that seems to reliably ensure that it can't happen was completely recreating my UITableView everytime the data changed as below. (So basically change everywhere where you call [self.tableView reloadData] with the following)
// The UITableView may in rare circumstances all of a sudden failed to render
// correctly. We're not entirely sure why this happens but its something to
// do with multiple threads executing and updating the data behind the view
// which somehow causes the events to stop firing. Resetting the delegate and
// dataSource to self isn't enough to fix things however so we have to
// completely recreate the UITableView and replace the existing one.
UITableView* tempTableView = [[[UITableView alloc] initWithFrame:CGRectMake(0, 0, 320, 387)] autorelease];
tempTableView.separatorColor = [UIColor grayColor];
tempTableView.backgroundColor = [UIColor clearColor];
if (self.tableView != nil)
{
[tempTableView setContentOffset:self.tableView.contentOffset animated:NO];
}
tempTableView.delegate = self;
tempTableView.dataSource = self;
[tempTableView reloadData];
if (self.tableView != nil) {
[self.tableView removeFromSuperview];
self.tableView = nil;
}
[self.view addSubview:tempTableView];
self.tableView = tempTableView;
I know this isn't the ideal fix and doesn't really explain the issue but I think this is an iOS bug. Would welcome any other input however.

Resources