Switching Tabs (initially) very slow - Move Function out of viewDidLoad? - iphone

In my iPhone app, I have three tabs laid out using a UITabBarController. The first tab (that loads on app launch) uses local data to load, and is very fast.
The second tab, though, which downloads an XML file from the web and parses it, then displays all the data in a UITableView, takes a long time to load over slower connections (EDGE, 3G). And, since I do call my parser inside viewDidLoad, the app won't switch to my second tab until everything is done—this means it takes a while to load the tab sometimes, and it looks like the app's locked up.
I'd rather be able to have the user switch to that tab, have the view load immediately—even if empty, and then have the data downloaded/parsed/displayed. I have the network activity spinner spinning, so at least the user can know something's happening.
Here's my current viewDidLoad:
// Load in the latest stories when the app is launched.
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(#"Loading news view");
articles = [[NSMutableArray alloc] init];
NSURL *url = [NSURL URLWithString:#"http://example.com/mobile-app/latest-news.xml"];
NSLog(#"About to parse URL: %#", url);
parser = [[NSXMLParser alloc] initWithContentsOfURL:url];
parser.delegate = self;
[parser parse];
}
I found this article, which shows how to run threads in the background, but I tried implementing that code and couldn't get the background thread to load the data back into my UITableView - the code was called, but how would I make sure the parsed articles are loaded back into my table view?

It turns out I needed to reload my UITableView after loading the data in a secondary thread, and that fixes everything!
Here's the final code in my secondary thread + viewDidLoad function:
-(void)initilizeNewsViewWithData {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Start the network activity spinner in the top status bar.
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
articles = [[NSMutableArray alloc] init];
NSURL *url = [NSURL URLWithString:#"http://example.com/mobile-app/latest-news.xml"];
parser = [[NSXMLParser alloc] initWithContentsOfURL:url];
parser.delegate = self;
[parser parse];
[tblLatestNews reloadData]; // Need to refresh the table after we fill up the array again.
[pool release];
}
// Load in the latest news stories in a secondary thread.
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelectorInBackground:#selector(initilizeNewsViewWithData) withObject:nil];
}
I was reminded of refreshing the table view from this answer: UITableView not displaying parsed data

Related

Block main thread until UIWebView finishes loading

I'm trying to block the main thread until a UIWebView finishes loading, but calling [NSThread sleepForTimeInterval:] after calling [UIWebView loadRequest:] makes the loading never complete: The webViewDidFinishLoad delegate is called after the sleep finishes.
Why is this happening? and How can I block the main thread?
I'm calling loadPage from [ViewController willRotateToInterfaceOrientation:], what I'm trying to do is to prevent the iOS interface to rotate until the webview with the other orientation is loaded.
- (void)loadPage {
UIWebView* webview2 = [[UIWebView alloc] initWithFrame:rect];
webview2.delegate = self;
NSString* htmlPath = ..... ; // Path to local html file
NSURL *newURL = [[[NSURL alloc] initFileURLWithPath: htmlPath] autorelease];
NSURLRequest *newURLRequest = [[[NSURLRequest alloc] initWithURL: newURL] autorelease];
[webview2 loadRequest:newURLRequest];
[NSThread sleepForTimeInterval:10.0f]; // Why this blocks the uiwebview thread ??
[self.view addSubview:webview2];
}
- (void)webViewDidFinishLoad:(UIWebView *)webView {
NSLog(#"Finished loading!");
}
I think you're trying to solve the wrong problem.
You don't want to block the main thread. Firstly, the entire UI is drawn on the main thread, so it will appear as though your app has hung. Secondly, a UIWebView is part of the UI (which is probably why what you're doing doesn't work).
So, what to do?
Have a look at the following method:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
When you start loading your page you can return NO from this method. When it's finished loading you can return values normally. UIWebView has a delegate that tells you the status.
Have a look at the class reference for NSURLRequest, there is a sync method in there.
+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error

UITableView reloadData causing all animations to stop working

Alright this is a really weird one so I am going to layout what is happening and then give some code after. For my example I am going to use a static amount of views, 2.
The Basics
I have a UIPageControl with X many Subviews added. On each subviews viewDidLoad is an NSXMLParse to grab an XML feed. Once the feed is obtained, it's parsed and the table is reloaded using the parsed array. There is also a Settings button on each view. When the Settings button is pressed, UIModalTransitionStyleCoverVertical:Animated:YES is run and a UINavigationController slides up into view with full animation. Dismiss also shows animation sliding out back to the previous view. If you are in Settings, you can PushViews two levels deep (Slide In Animation).
The Problem
A random amount of the time, when the app is built and run (Not Resumed) when you tap the Settings button, the Animation does not occur. Everything is functional except for all Core Animations are removed. DismissModal simply swaps back to the previous screen. PushView in the NavigationController no longer has any animation, the next view simply appears.
If you quit the app (Kill Process) and relaunch it, it may work fine for a period of time but at some point when you tap the Settings button, it will lose all animations.
The Details
I started with Apples PageControl Application for the groundwork. It creates a dynamic amount of views based on user settings.
- (void)awakeFromNib
{
kNumberOfPages = 2;
// view controllers are created lazily
// in the meantime, load the array with placeholders which will be replaced on demand
NSMutableArray *controllers = [[NSMutableArray alloc] init];
for (int i = 0; i < kNumberOfPages; i++)
{
[controllers addObject:[NSNull null]];
}
self.viewControllers = controllers;
// a page is the width of the scroll view
scrollView.pagingEnabled = YES;
scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * kNumberOfPages, scrollView.frame.size.height);
scrollView.showsHorizontalScrollIndicator = NO;
scrollView.showsVerticalScrollIndicator = NO;
scrollView.scrollsToTop = NO;
scrollView.delegate = self;
pageControl.numberOfPages = kNumberOfPages;
pageControl.currentPage = 0;
// pages are created on demand
// load the visible page
// load the page on either side to avoid flashes when the user starts scrolling
[self loadScrollViewWithPage:0];
[self loadScrollViewWithPage:1];
}
- (void)loadScrollViewWithPage:(int)page
{
if (page < 0)
return;
if (page >= kNumberOfPages)
return;
// replace the placeholder if necessary
SecondViewController *controller = [viewControllers objectAtIndex:page];
if ((NSNull *)controller == [NSNull null])
{
controller = [[SecondViewController alloc] initWithPageNumber:page];
[viewControllers replaceObjectAtIndex:page withObject:controller];
[controller release];
}
// add the controller's view to the scroll view
if (controller.view.superview == nil)
{
CGRect frame = scrollView.frame;
frame.origin.x = frame.size.width * page;
frame.origin.y = 0;
controller.view.frame = frame;
[scrollView addSubview:controller.view];
}
}
As each view is generated, it runs an NSXMLParse in its viewDidLoad. Everything works fine up to this point. Both views are generated and you can swipe between them.
If you push the Settings Button
- (IBAction)settingsButtonPressed:(id)sender;
{
SettingsViewController *settingsViewController = [[SettingsViewController alloc] initWithNibName:#"SettingsViewController" bundle:nil];
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:settingsViewController];
navigationController.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self presentModalViewController:navigationController animated:YES];
[settingsViewController release];
[navigationController release];
}
At this point, SettingsViewController appears into view. However, sometimes it slides up with its proper animation. Other times it will simply appear and all further core animations are broken until the process is restarted.
I went through and checked all of NSXMLParse and have narrowed down the problem to one line. On each of my subviews, is a tableView, after the XML Parsing is done, I created an array with the results and ran [self.tableview reloadData]. If I comment out that line, the table obviously only loads blank but it doesn't have any issues with Animations.
- (void)parserDidEndDocument:(NSXMLParser *)parser
{
NSMutableArray *tableData = ARRAY_GENERATED_HERE;
[self.tableView reloadData];
}
My Testing
I will note from my tests, everything is fine if kNumberOfPages is set to 1 instead of 2. Only 1 view gets generated, the Animation glitch never occurs. Add a second view in, usually within opening Settings five times, it will glitch.
Still haven't come to a solution but it has to do with [tableView reloadData]. Any insight would be great.
Daniel pointed out something that makes sense.
My XML is fetched in the viewDidLoad using:
[NSThread detachNewThreadSelector:#selector(parseXMLFileAtURL:) toTarget:self withObject:path];
- (void)parseXMLFileAtURL:(NSString *)URL
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
stories = [[NSMutableArray alloc] init];
//you must then convert the path to a proper NSURL or it won't work
NSURL *xmlURL = [NSURL URLWithString:URL];
// here, for some reason you have to use NSClassFromString when trying to alloc NSXMLParser, otherwise you will get an object not found error
// this may be necessary only for the toolchain
rssParser = [[NSXMLParser alloc] initWithContentsOfURL:xmlURL];
// Set self as the delegate of the parser so that it will receive the parser delegate methods callbacks.
[rssParser setDelegate:self];
// Depending on the XML document you're parsing, you may want to enable these features of NSXMLParser.
[rssParser setShouldProcessNamespaces:NO];
[rssParser setShouldReportNamespacePrefixes:NO];
[rssParser setShouldResolveExternalEntities:NO];
[rssParser parse];
[pool release];
}
From your comment, you said you are running the parser in a background thread...UIKit is not thread safe and i suspect that is whats causing your problems...try making the reloadData call on the main thread, you can use NSObjects performSelectorInMainThread to do this...
[self performSelectorOnMainThread:#selector(operationComplete) withObject:nil waitUntilDone:false];

how to solve error when using thread?

I have following error msg in console when using NSThread
"Tried to obtain the web lock from a thread other than the main thread or the web thread. This may be a result of calling to UIKit from a secondary thread. Crashing now..."
I have submit my sample code here
- (void)viewDidLoad {
appDeleg = (NewAshley_MedisonAppDelegate *)[[UIApplication sharedApplication] delegate];
[[self tblView1] setRowHeight:80.0];
[super viewDidLoad];
self.title = #"Under Ground";
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
[NSThread detachNewThreadSelector:#selector(CallParser) toTarget:self withObject:nil];
}
-(void)CallParser {
Parsing *parsing = [[Parsing alloc] init];
[parsing DownloadAndParseUnderground];
[parsing release];
[self Update_View];
//[myIndicator stopAnimating];
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
}
here "DownloadAndParseUnderground" is the method of downloding data from the rss feed and
-(void) Update_View{
[self.tblView1 reloadData];
}
when Update_View method is called the tableView reload Data and in the cellForRowAtIndexPath create error and not display custom cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
CustomTableviewCell *cell = (CustomTableviewCell *) [tblView1 dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
[[NSBundle mainBundle] loadNibNamed:#"customCell"
owner:self
options:nil];
cell = objCustCell;
objCustCell = nil;
}
if there is a crash, there is a backtrace. Please post it.
Method names start with lowercase letters, are camelCased, and do not contain underscores. Following these conventions will make your code easier to read by other iOS programmers and learning these conventions will make it easier to understand other iOS programmer's code.
You can't directly or indirectly invoke methods in the main thread from background threads. Your crash and your code both indicate that you are freely interacting with the main thread form non-main threads.
The documentation on the use of threads in iOS applications is quite extensive.
Your problem should come because you load your UIViewController from a thread that's not the main thread. Tipically when you try to charge data before loading the view.
To arrange this you can try to do this
1. Add a method to load your viewcontroller with just one param
-(void)pushViewController:(UIViewController*)theViewController{
[self.navigationController pushViewController:theViewController animated:YES];}
2.Change your code (commented below) in your asynchronous loading to "PerformSelectorOnMainThread"
-(void)asyncLoadMyViewController
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
MyViewController *myVC = [[myVC alloc] initWithNibName:#"myVC" bundle:nil ];
[self performSelectorOnMainThread:#selector(pushViewController:) withObject:myVC waitUntilDone:YES];
// [self.navigationController pushViewController:wnVC animated:YES];
[wnVC release];
[pool release];
}
ok please explain proper Why you require thread in parsing method? in your code u use table reload method in properly in thread....
because
u cant put any thing which relavent to your VIEW in thread...
u can put only background process like parsing in it... if u want reload table after parsing u can use some flag value in your code and after parsing u load table
Try change CallParser method to
-(void)CallParser {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
Parsing *parsing = [[Parsing alloc] init];
[parsing DownloadAndParseUnderground];
[self performSelectorOnMainThread:#selector(Update_View)
withObject:nil
waitUntilDone:NO];
[parsing release];
[pool release];
}
And move
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
line to Update_View method
You can't access any UI elements from a background thread. You certainly can't create views on a background thread. Use "performSelectorOnMainThread" method instead of "detachNewThreadSelector" method.
All the best.

Send message to a different class (Obj C)

I have a UITableViewController (OnTVViewController) who's viewDidLoad is similar to below (basically parses some XML in the background and shows an activity indicator while this is happening).:
- (void)viewDidLoad {
OnTVXMLParser *xmlParser = [[OnTVXMLParser alloc] init];
/* Runs the parse command in the background */
[NSThread detachNewThreadSelector:#selector(parse) toTarget:xmlParser withObject:self];
//[xmlParser parse];
// new view to disable user interaction during downloading.
loadView = [[UIView alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
loadView.backgroundColor = [UIColor darkGrayColor];
//Loader spinner
UIActivityIndicatorView *act = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
[loadView addSubview:act];
act.center =loadView.center;
[self.view addSubview:loadView];
[self.view bringSubviewToFront:loadView];
[act startAnimating];
[act release];
[super viewDidLoad];
}
OnTVViewController also has this method to remove the activity indicator (just trying to log a message while debugging):
- (void)removeActivityView {
//[loadView removeFromSuperview];
NSLog(#"Should remove activity view here");
}
In my OnTVXMLParser class I have:
- (BOOL)parse{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSLog(#"Sleeping for 5 seconds");
[NSThread sleepForTimeInterval:5.0];
NSLog(#"Sleep finished");
// Simulated some elapsed time. I want to remove the Activity View
[self performSelectorOnMainThread:#selector(removeActivityView) withObject:nil waitUntilDone:false];
// Create and initialize an NSURL with the RSS feed address and use it to instantiate NSXMLParser
NSURL *url = [[NSURL alloc] initWithString:#"http://aurl.com/xml"];
NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL:url];
// Lots of parsing stuff snipped, this all runs fine
[pool release];
return YES;
}
Basically once the "parse" method on the XMLParser class has finished I want to call the removeActivityIndicator on the OnTVViewController object. It's probably really simple but I am new to iPhone programming and banging my head against the wall.
I understand I need to use performSelectorOnMainThread - but how do I reference the instance of OnTVViewController I want to target? I've imported the OnTVViewController header file into OnTVXMLParser.
At the moment I get the error:
-[OnTVViewController removeActivityView:]: unrecognized selector sent to instance 0x8840ba0'
Typically cocoa handles this with a delegate pattern. Basically add an ivar to the XML parser named delegate, and launching the parade set the delegate to self (the OnTVViewController), and then later use the delegate for all callbacks in the XML parser
The problem is that the selector called:
-[OnTVViewController removeActivityView:]: unrecognized selector sent to instance 0x8840ba0'
does not exist. You tell it to call this selector:
[self performSelectorOnMainThread:#selector(removeActivityView) withObject:nil waitUntilDone:false];
Which doesn't exist (note the :), because your method is named:
- (void)removeActivityView
Try calling it
- (void)removeActivityView:(id)dummy
and see what happens.
You might also want to consider using NSNotificationCenter. This allows you to add an observer for a given key (a string) and selector, then post a notification from other parts of your application.
Examples:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(methodToCall:) name:#"SomeKeyName" object:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:#"SomeKeyName" object:nil];

Why does my UIActivityIndicatorView only display once?

I'm developing an iPhone app that features a tab-bar based navigation with five tabs. Each tab contains a UITableView whose data is retrieved remotely. Ideally, I would like to use a single UIActivityIndicatorView (a subview of the window) that is started/stopped during this remote retrieval - once per tab.
Here's how I set up the spinner in the AppDelegate:
- (void)applicationDidFinishLaunching:(UIApplication *)application {
[window addSubview:rootController.view];
activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
[activityIndicator setCenter:CGPointMake(160, 200)];
[window addSubview:activityIndicator];
[window makeKeyAndVisible];
}
Since my tabs were all performing a similiar function, I created a base class that all of my tabs' ViewControllers inherit from. Here is the method I'm using to do the remote retrieval:
- (void)parseXMLFileAtURL:(NSString *)URL {
NSAutoreleasePool *apool = [[NSAutoreleasePool alloc] init];
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSLog(#"parseXMLFileAtURL started.");
[appDelegate.activityIndicator startAnimating];
NSLog(#"appDelegate.activityIndicator: %#", appDelegate.activityIndicator);
articles = [[NSMutableArray alloc] init];
NSURL *xmlURL = [NSURL URLWithString:URL];
rssParser = [[NSXMLParser alloc] initWithContentsOfURL:xmlURL];
[rssParser setDelegate:self];
[rssParser setShouldProcessNamespaces:NO];
[rssParser setShouldReportNamespacePrefixes:NO];
[rssParser setShouldResolveExternalEntities:NO];
[rssParser parse];
NSLog(#"parseXMLFileAtURL finished.");
[appDelegate.activityIndicator stopAnimating];
[apool release];
}
This method is being called by each view controller as follows:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if ([articles count] == 0) {
NSString *path = #"http://www.myproject.com/rss1.xml";
[self performSelectorInBackground:#selector(parseXMLFileAtURL:) withObject:path];
}
}
This works great while the application loads the first tab's content. I'm presented with the empty table and the spinner. As soon as the content loads, the spinner goes away.
Strangely, when I click the second tab, the NSLog messages from the -parseXMLFileAtURL: method show up in the log, but the screen hangs on the first tab's view and I do not see the spinner. As soon as the content is done downloading, the second tab's view appears.
I suspect this has something to do with threading, with which I'm still becoming acquainted. Am I doing something obviously wrong here?
You should perform all actions on the activity indicator on the main thread using:
performSelectorOnMainThread:withObject:waitUntilDone: