I've got a UITableView with several entries. Upon selecting one, I need it to do a potentially time-consuming network operation. To give the user some feedback, I tried to place a UIActivityIndicatorView in the UITableViewCell. However, the spinner doesn't appear until much later -- after I've done the expensive operation! What am I doing wrong?
- (NSIndexPath *) tableView:(UITableView *) tableView
willSelectRowAtIndexPath:(NSIndexPath *) indexPath {
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc]
initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
[spinner autorelease];
[spinner startAnimating];
[[tableView cellForRowAtIndexPath:indexPath] setAccessoryView:activity];
if ([self lengthyNetworkRequest] == nil) {
// ...
return nil;
}
return indexPath;
}
As you can see, I have the spinner being set to the accessoryView before the lengthy network operation. But it only appears after the tableView:willSelectRowAtIndexPath: method finishes.
Once you tell the ActivityIndicator to start animating, you have to give your application's run loop a chance to start the animation before starting the long operation. This can be achieved by moving the expensive code into its own method and calling:
[self performSelector:#selector(longOperation) withObject:nil afterDelay:0];
EDIT: I think you should be using didSelect instead of willSelect.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
Try adding [CATransaction flush] right before
if ([self lengthyNetworkRequest] == nil) {
- (NSIndexPath *) tableView:(UITableView *) tableView
willSelectRowAtIndexPath:(NSIndexPath *) indexPath {
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc]
initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
[spinner autorelease];
[spinner startAnimating];
[[tableView cellForRowAtIndexPath:indexPath] setAccessoryView:activity];
if ([self lengthyNetworkRequest] == nil) {
//doing the intensive work after a delay so the UI gets updated first
[self performSelector:#selector(methodThatTakesALongTime) withObject:nil afterDelay:0.25];
//you could also choose "performSelectorInBackground"
}
return indexPath;
}
- (void)methodthatTakesALongTime{
//do intensive work here, pass in indexpath if needed to update the spinner again
}
Related
I have two UITableViewControllers, A and B. When I tap one cell in table A, I will use UINavigationController to push table view controller B. But the data of B is downloaded from Internet, which takes several seconds. So I want to add a UIActivityIndicatorView when loading B. How can I achieve this?
You can add UIActivityIndicatorView as cell's accessoryView.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
spinner.frame = CGRectMake(0, 0, 24, 24);
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.accessoryView = spinner;
[spinner startAnimating];
[spinner release];
}
In viewDidLoad of tableview B class, add an activity indicator.
// Create the Activity Indicator.
let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
activityIndicator.hidesWhenStopped = true
view.addSubview(activityIndicator)
// Position it at the center of the ViewController.
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)])
activityIndicator.startAnimating()
Now call your method that downloads data from the network.
myDownloadMethod()
Do it in a different thread if you don't want the UI to be non responsive during the process.
read this thread for that.
Can I use a background thread to parse data?
When you are notified that the contents are downloaded, stop the indicator.
activityIndicator.stopAnimating()
Now you can call tableview.reloadData() for reloading the table to display the new contents.
UIActivityIndicatorView * activityindicator1 = [[UIActivityIndicatorView alloc]initWithFrame:CGRectMake(150, 200, 30, 30)];
[activityindicator1 setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleWhiteLarge];
[activityindicator1 setColor:[UIColor orangeColor]];
[self.view addSubview:activityindicator1];
[activityindicator1 startAnimating];
[self performSelector:#selector(callfunction) withObject:activityindicator1 afterDelay:1.0];
-(void)callfunction
{
// Here your stuf
}
It's work well for me, you can try it:
Call [activityIndicator startAnimating] when didHighlightRowAtIndexPath,
and call [activityIndicator stopAnimating] when didUnhighlightRowAtIndexPath useful than didSelectRowAtIndexPath.
- (void)runIndicatorAtIndexPath:(NSIndexPath *)indexPath display:(BOOL)playing{
UIActivityIndicatorView *activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
cell.accessoryView = activityIndicator;
playing == YES ?[activityIndicator startAnimating]:[activityIndicator stopAnimating];
}
- (void)tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(NSIndexPath *)indexPath {
[self runIndicatorAtIndexPath:indexPath display:YES];
}
- (void)tableView:(UITableView *)tableView didUnhighlightRowAtIndexPath:(NSIndexPath *)indexPath{
[self runIndicatorAtIndexPath:indexPath display:NO];
}
This code below will display a spinner at the footer of the table view if more data is available on the server. You can change it according to your logic of fetching data from server.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
/* set cell attributes here */
NSInteger lastSectionIndex = [tableView numberOfSections] - 1;
NSInteger lastRowIndex = [tableView numberOfRowsInSection:lastSectionIndex] - 1;
if ((indexPath.section == lastSectionIndex) && (indexPath.row == lastRowIndex)) {
if(isMoreDataAvailableOnserver)
{
[self showSpinnerAtFooter];
[self getMoreDataFromServer];
}
else {
[self hideSpinnerAtFooter];
}
}
return cell;
}
- (void)hideSpinnerAtFooter {
self.tableView.tableFooterView = nil;
}
- (void)showSpinnerAtFooter {
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[spinner startAnimating];
spinner.frame = CGRectMake(0, 0, 320, 44);
self.tableView.tableFooterView = spinner;
}
I can successfully push only next view in my iPhone app. However, cause the next view retrieves data to populate the UITableViews, sometimes waiting time could be a few seconds or slightly longer depending on connection.
During this time, the user may think the app has frozen etc. So, to counter this problem, I think implementing UIActivityIndicators would be a good way to let the user know that the app is working.
Can someone tell me where I could implement this?
Thanks.
pushDetailView Method
- (void)pushDetailView {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
//load the clicked cell.
DetailsImageCell *cell = (DetailsImageCell *)[tableView cellForRowAtIndexPath:indexPath];
//init the controller.
AlertsDetailsView *controller = nil;
if(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad){
controller = [[AlertsDetailsView alloc] initWithNibName:#"DetailsView_iPad" bundle:nil];
} else {
controller = [[AlertsDetailsView alloc] initWithNibName:#"DetailsView" bundle:nil];
}
//set the ID and call JSON in the controller.
[controller setID:[cell getID]];
//show the view.
[self.navigationController pushViewController:controller animated:YES];
You can implement this in didSelectRowAtIndexPath: method itself.
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle: UIActivityIndicatorViewStyleWhiteLarge];
cell.accessoryView = spinner;
[spinner startAnimating];
[spinner release];
This will show the activity indicator at the right side of the cell which will make the user feel that something is loading.
Edit: If you set the accessoryView and write the loading code in the same method, the UI will get updated only when all the operations over. A workaround for this is to just set the activityIndicator in the didSelectRowAtIndexPath: method and call the view controller pushing code with some delay.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle: UIActivityIndicatorViewStyleWhiteLarge];
cell.accessoryView = spinner;
[spinner startAnimating];
[spinner release];
[self performSelector:#selector(pushDetailView:) withObject:tableView afterDelay:0.1];
}
- (void)pushDetailView:(UITableView *)tableView {
// Push the detail view here
}
I have the following error. 'indexPath' undeclared (first use in this function).
Code.
didSelectRowAtIndexPath
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle: UIActivityIndicatorViewStyleWhiteLarge];
cell.accessoryView = spinner;
[spinner startAnimating];
[spinner release];
[self performSelector:#selector(pushDetailView:) withObject:tableView afterDelay:0.1];
}
pushDetailView
- (void)pushDetailView:(UITableView *)tableView {
// Push the detail view here
[tableView deselectRowAtIndexPath:indexPath animated:YES];
//load the clicked cell.
DetailsImageCell *cell = (DetailsImageCell *)[tableView cellForRowAtIndexPath:indexPath];
//init the controller.
AlertsDetailsView *controller = nil;
if(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad){
controller = [[AlertsDetailsView alloc] initWithNibName:#"DetailsView_iPad" bundle:nil];
} else {
controller = [[AlertsDetailsView alloc] initWithNibName:#"DetailsView" bundle:nil];
}
//set the ID and call JSON in the controller.
[controller setID:[cell getID]];
//show the view.
[self.navigationController pushViewController:controller animated:YES];
}
I think it's because I'm not parsing the indexPath value from didSelectRowAtIndexPath to pushDetailView but I don't know how to approach this.
Could someone please advise?
Thanks.
The problem is that you pushDetailView: method has no indexPath variable on it's scope.
Instead of
- (void)pushDetailView:(UITableView *)tableView {
You should make your method like this:
- (void)pushDetailView:(UITableView *)tableView andIndexPath: (NSIndexPath*) indexPath {
and then indexPath would be declared on the method scope.
Then, inside your didSelectRowAtIndexPath method, replace
[self performSelector:#selector(pushDetailView:) withObject:tableView afterDelay:0.1];
for the code bellow:
double delayInSeconds = 0.1;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self pushDetailView: tableView andIndexPath: indexPath];
});
This uses GCD to execute the code after a delay, instead of the performSelector: withObject :afterDelay: and here is a nice post explaining why sometimes is better to opt for this aproach
Since you need to provide two arguments and perform after a delay package both arguments in an NSDictionary and pass it:
NSDictionary *arguments = [NSDictionary dictionaryWithObjectsAndKeys:
tableView, #"tableView", indexPath, #"indexPath", nil];
[self performSelector:#selector(pushDetailView:) withObject:arguments afterDelay:0.1];
...
- (void)pushDetailView:(NSDictionary *)arguments {
UITableView *tableView = [arguments objectForKey:#"tableView"];
NSIndexPath *indexPath = [arguments objectForKey:#"indexPath"];
...
Or as #Felipe suggests, use GCD.
After retrieving search results from a UISearchBar, the results appear in my tableview correctly, but the view is 'greyed out' (see image below)..Any help on this is appreciated, I can't find a solution the Apple documentation.
This is my code that is fired upon hitting the Search button:
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
isSearchOn = YES;
canSelectRow = YES;
self.tableView.scrollEnabled = YES;
CityLookup *cityLookup = [[CityLookup alloc] findCity:searchBar.text];
if ([cityLookup.citiesList count] > 0) {
tableCities = cityLookup.citiesList;
}
[cityLookup release];
isSearchOn = NO;
self.searchBar.text=#"";
[self.searchBar setShowsCancelButton:NO animated:YES];
[self.searchBar resignFirstResponder];
[self.navigationController setNavigationBarHidden:NO animated:YES];
[self.tableView reloadData];
}
And this is how the table view is being refreshed:
-(UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *kCellID = #"cellID";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellID];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCellID] autorelease];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
NSString *cellValue = [tableCities objectAtIndex:indexPath.row];
cell.textLabel.text = cellValue;
return cell;
}
If you are using the full package with a UISearchDisplayController the following line should remove the search interface:
[self.searchDisplayController setActive:NO animated:YES];
If you are not using UISearchDisplayController I would recommend checking it out and see if it doesn't suit your needs.
Remark: There is nothing in your posted code that would remove whatever view you have used to grey out the table view. So if you are not using UISearchDisplayController look in the code that displays the search interface to see what you need to do to remove it.
Your not hiding your search bar when hitting search, try:
[self.navigationController setNavigationBarHidden:YES animated:YES];
I have a an app that has a UITabBarController.
What I would like to achieve, is that the first ViewController included in the TabBar displays a TableView if there are items in the array property (loaded from CoreData), or a UIImageView (with more information about how to add items) if not.
If the user goes to a different TabBarItem, and comes back to the first TabBarItem the array could have been populated. So it should change what is displayed accordingly.
Could somebody post a programatic snippet to achieve this. I have tried several things, and none have worked properly.
[self performSelectorInBackground:#selector(loadRecentInBackground) withObject:nil];
Then defining the selector
- (void) loadRecentInBackground{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
// Define put the values into the data structure that you retrieve
// (I use an NSMutableDictionary)
[pool release];
}
and retrieve the value from the dictionary in
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
and make sure to add the:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
where you return the count of the data structure.
The easiest way would be to set a flag based on whether the array is populated or not. Then adjusting the table to display either rows of data or a single row displaying an UIImageView with the instructions.
It would be best to set the flag in -viewWillDisplay and the format the table by returning the correct values based on the flag from – tableView:heightForRowAtIndexPath:, – tableView:willDisplayCell:forRowAtIndexPath: and – numberOfSectionsInTableView:.
This code does the job
- (void)loadView {
UIView* aView = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
//tableView
self.tableView = [[[UITableView alloc ] initWithFrame:CGRectMake(0,0,320,460) style:UITableViewStylePlain] autorelease];
self.tableView.backgroundColor = [UIColor clearColor];
self.tableView.autoresizingMask = (UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight);
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.autoresizesSubviews = YES;
[aView addSubview: tableView];
self.view = aView;
[aView release];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self loadData];
//Display or remove Image depending on the ammount of results.
if ([self.itemsArray count] == 0) {
NSLog(#"No Items in Array");
if (noDataImageView == nil) {
noDataImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"NoDataBackground.png"]];
[self.view addSubview:noDataImageView];
}
}else{
if(noDataImageView != nil){
[noDataImageView removeFromSuperview];
}
}
}
- (void)dealloc {
[super dealloc];
[tableView release];
[noDataImageView release];
}