I am implementing a simple in-app browser. In my home view (UITableViewController), I have something like:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
WebViewController *webViewController = [[WebViewController alloc] init];
switch (indexPath.row) {
case 0:
webViewController.stringURL = #"http://www.google.com";
break;
case 1:
webViewController.stringURL = #"http://www.bing.com";
break;
default:
webViewController.stringURL = #"http://stackoverflow.com";
break;
}
[self.navigationController pushViewController:webViewController animated:YES];
[webViewController release];
}
The app crashed after I repetitively navigated back and forth between my home view and webViewControllera few times.
Inside WebViewController class, I have nothing but a [UIWebView *webView] and a [UIActivityIndicatorView *activityIndicator]. Both are with attributes nonatomic, retain. Here is the implementation.
#import "WebViewController.h"
#implementation WebViewController
#synthesize webView, activityIndicator, stringURL;
- (void)dealloc
{
[self.webView release];
self.webView.delegate = nil;
[self.activityIndicator release];
[super dealloc];
}
-(void)loadView {
UIView *contentView = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
self.view = contentView;
CGRect webFrame = [[UIScreen mainScreen] applicationFrame];
webFrame.origin.y = 0.0f;
self.webView = [[UIWebView alloc] initWithFrame:webFrame];
self.webView.backgroundColor = [UIColor blueColor];
self.webView.scalesPageToFit = YES;
self.webView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
self.webView.delegate = self;
[self.view addSubview: self.webView];
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:self.stringURL]]];
self.activityIndicator = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
self.activityIndicator.frame = CGRectMake(0.0, 0.0, 30.0, 30.0);
self.activityIndicator.center = self.view.center;
[self.view addSubview: self.activityIndicator];
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self loadView];
}
- (void)webViewDidStartLoad:(UIWebView *)webView
{
// starting the load, show the activity indicator in the status bar
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
[activityIndicator startAnimating];
}
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
// finished loading, hide the activity indicator in the status bar
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
[activityIndicator stopAnimating];
}
#end
I just ran my app in Instruments using the Zombies template, which shows -[UIWebView webView:didReceiveTitle:forFrame:] is the Zombie call. But I still can’t figure out what is actually the problem.
(Please download trace if needed)
Any help is greatly appreciated!
[Update]:
As #7KV7 and #David pointed out, there is an obvious bug in my dealloc function. I should call self.webView.delegate=nil; first before I release self.webView. Sorry about that. Unfortunately, after I fix it, the app still crashes in the same way.
If I delete [webViewController release]; from the first code block, the crash actually is gone. But obviously, there will be memory leak.
First of all, remove that call to loadView in viewDidLoad. The framework will the call the method when it doesn't find a view provided in XIB file. Second, your loadView is filled with memory leaks. You are allocating, initializing and retaining an object every time the method is called. So you are taking ownership twice and releasing it only once in the dealloc.
The objects are not being properly deallocated. You should do something like alloc-init-autorelease to solve this. Next thing is the that every time the controller gets loaded, because of your call to loadView, you end up creating two web view objects and two requests. You lose reference to one of them as you reassign. Herein, lies the problem mentioned in the title. You aren't able to reset the delegate of a web view object that has your controller as a delegate. Imagine a request being completed soon after you leave. Here the message will go to a zombie object. This is a pretty good example for why you need to nil out your delegates.
- (void)dealloc
{
self.webView.delegate = nil;
[self.webView release];
[self.activityIndicator release];
[super dealloc];
}
Try this dealloc. You were releasing the webview and then setting the delegate as nil. You should first set the delegate as nil and then release it. Hope this solves the issue.
I think what's happening is that you are going back while the page is still loading so the controller gets deallocated and then the webview finishes loading.
Try calling [webView stopLoading] in your viewDidUnload method to make sure this isn't happening.
Don't know if it's the cause of your problem, but this is definitely wrong:
[self.webView release];
self.webView.delegate = nil;
You cannot (safely) refer to self.webView after you release it!
Instead of pushing webViewController,add its view to self.view .
Dont call [self loadView]; in viewDidLoad.
Related
I am trying to push an opengl UIView to my navigation controller like this
GraphViewController *gvc = [[GraphViewController alloc] initWithTicker:[listOfItems objectAtIndex:indexPath.row]];
[self.navigationController pushViewController:gvc animated:YES];
[gvc release];
The initWithTicker method looks like this
-(id) initWithTicker:(NSString*)ticker{
self = [super initWithNibName:nil bundle:nil];
if (self) {
self.title = ticker;
EAGLView *eagl = [[EAGLView alloc] initWithFrame:[UIScreen mainScreen].bounds];
eagl.animationInterval = 1.0 / 60.0;
[eagl startAnimation];
self.view = eagl;
}
return self;
}
When I go back and forward in my UINavigationController, the drawView method (in EAGLView) keeps looping. Furthermore, if I pushViewController again, the first one does not stop and a new one is created! I've tried making this an instance variable so only one is created and it has the same effect. I would be grateful if anyone has insight as to why this is happening
sergio Suggestion:
-(id) initWithTicker:(NSString*)ticker{
self = [super initWithNibName:nil bundle:nil];
if (self) {
self.title = ticker;
}
return self;
}
// Implement loadView to create a view hierarchy programmatically, without using a nib.
- (void)loadView {
eagl = [[EAGLView alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.view = eagl;
}
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
eagl.animationInterval = 1.0 / 60.0;
[eagl startAnimation];
[super viewDidLoad];
}
same behaviour.
---This is how I fixed my drawView looping problem--
-(void)viewDidAppear:(BOOL)animated {
[eagl startAnimation];
[super viewDidAppear:animated];
}
-(void)viewDidDisappear:(BOOL)animated {
[eagl stopAnimation];
[super viewDidDisappear:animated];
}
--Craigs solution --
if(graphView == nil){
graphView = [[GraphViewController alloc] initWithTicker:[listOfItems objectAtIndex:indexPath.row]];
}else{
[graphView release];
graphView = [[GraphViewController alloc] initWithTicker:[listOfItems objectAtIndex:indexPath.row]];
}
Are you creating a new GraphViewController every time you want to push one onto your navigation stack? If so, it doesn't really matter how you're handling the creation of your EAGLView instance variable, since you're never going to be interacting with that view controller again anyway.
For example:
User taps something, a new GraphViewController is pushed on the stack
User goes back, this view controller continues to run
Return to 1. and repeat (thus creating a SECOND GraphViewController, and then a third, and then a fourth... etc.)
What you should probably be doing is maintaining your GraphViewController as an instance variable, and only creating it once. This will ensure that you're in turn only creating one EAGLView.
if (_graphViewController == nil) {
_graphViewController = [[GraphViewController alloc] initWithTicker:[listOfItems objectAtIndex:indexPath.row]];
}
[self.navigationController pushViewController:_graphViewController animated:YES];
Then, be sure to release the view controller in your dealloc method if you're going to be maintaining it as an ivar.
Would you try executing this code of yours:
EAGLView *eagl = [[EAGLView alloc] initWithFrame:[UIScreen mainScreen].bounds];
eagl.animationInterval = 1.0 / 60.0;
[eagl startAnimation];
self.view = eagl;
inside of loadView? I am not sure about why your view is behaving like you say, but that is the place where you are supposed to build your UI... so it might make a difference...
Furthermore, I would call [eagl startAnimation]; only in viewDidLoad...
Hope it helps...
I just trying to get to QLPreviewController.view. Indeed, I want to catch a tap event on its view to show/hide toolbar etc. I am trying:
QLPreviewController* qlpc = [QLPreviewController new];
qlpc.delegate = self;
qlpc.dataSource = self;
qlpc.currentPreviewItemIndex=qlIndex;
[navigator pushViewController:qlpc animated:YES];
qlpc.title = [path lastPathComponent];
[qlpc setToolbarItems:[NSArray arrayWithObjects:self.dirBrowserButton,self.space, self.editButton, self.btnSend, nil] animated:YES];
UITapGestureRecognizer* gestTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(showControls:)];
gestTap.cancelsTouchesInView=NO;
[qlpc.view addGestureRecognizer:[gestTap autorelease]];
[qlpc release];
And nothing happens
If I attach UITapRecognizer onto navigationController.view, it fires only if I touch toolbar/navbar. UISwipeGestureRecognizer works fine in that case.
I tried to attach a transparent overlay view and add gesture recognizers on it, but no luck.
Well, I saw some apps that implements such a feature so obviously it is possible, but how?
Sorry, I googled all day long and didn't find any solution. Please, help me.
With your solution, does the QLPreviewController's view still recieve touches? I've tried to do something similar (I'm stealing the view from QLPreviewController to use it) and it looks like my overlay view doesn't let anything pass trough to the view lying behind it.
I have been working on this problem today and the suggestion to override -(void)contentWasTappedInPreviewContentController:(id)item {} is close but when you do you mess with the preview controllers handling.
So I stopped overriding that method and instead created a RAC signal that fires whenever the method is called. This does not mess with the default behavior of QL. I am doing it in a subclass of the QLPreviewController but that shouldn't be necessary.
I have a property on my class:
#property RACSignal *contentTapped;
Then in my init method of my subclass of QLPreviewController:
_contentTapped = [self rac_signalForSelector:#selector(contentWasTappedInPreviewContentController:)];
Now in another class or even internally you can use the signal like this:
previewController.contentTapped subscribeNext:^(id x) {
// Put your handler here!
}];
Here is my solution (to use KVO), where I'm monitoring navigation bar status - and showing toolbar when needed (it seems that it hides toolbar by itself when tapped)
#define kNavigationBarKeyPath #"navigationBar.hidden"
static void * const NavigationBarKVOContext = (void*)&NavigationBarKVOContext;
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.navigationController setToolbarHidden:NO];
[self.navigationController addObserver:self forKeyPath:kNavigationBarKeyPath options:NSKeyValueObservingOptionPrior context:NavigationBarKVOContext];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.navigationController removeObserver:self forKeyPath:kNavigationBarKeyPath];
}
And
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ( context == NavigationBarKVOContext ) {
BOOL prior = [change[NSKeyValueChangeNotificationIsPriorKey] boolValue];
if ( prior && self.navigationController.toolbarHidden ) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.navigationController setToolbarHidden:NO animated:YES];
});
}
}
}
I found none of the answers here to work, but the one that did for me was to subclass QLPreviewController and override viewDidAppear as so:
- (void)viewDidAppear:(BOOL)animated
{
UITapGestureRecognizer *gestTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(showControls:)];
gestTap.cancelsTouchesInView = NO;
[self.view addGestureRecognizer:[gestTap autorelease]];
}
Ok, solution is very simple.
just added an overlay view onto keyWindow. Attached gesture recognizers onto overlay and it works.
QLPreviewController* qlpc = [QLPreviewController new];
qlpc.delegate = self;
qlpc.dataSource = self;
qlpc.currentPreviewItemIndex=qlIndex;
[navigator pushViewController:qlpc animated:YES];
qlpc.title = [path lastPathComponent];
UIView* overlay = [[[UIView alloc] initWithFrame:navigator.view.bounds] autorelease];
[[[UIApplication sharedApplication] keyWindow] addSubview:overlay];
[overlay setNeedsDisplay];
[qlpc setToolbarItems:[NSArray arrayWithObjects:self.dirBrowserButton,self.space, self.editButton, self.btnSend, nil] animated:YES];
UITapGestureRecognizer* gestTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(showControls:)];
gestTap.cancelsTouchesInView=NO;
[overlay addGestureRecognizer:[gestTap autorelease]];
[qlpc release];
Subclass QLPreviewController and then override
-(void)contentWasTappedInPreviewContentController:(id)item
{}
Thats its !
I am trying to implement AQGridView based upon the ImageDemo in the /examples folder. I have a view controller with the following declaration:
#interface ImageDemoViewController : UIViewController <AQGridViewDelegate, AQGridViewDataSource, ImageDemoCellChooserDelegate>
{
...
None of the datasource methods in my view controller such as
- (NSUInteger) numberOfItemsInGridView: (AQGridView *) aGridView
{
return ( [images count] );
}
are being called. Here is where I setup the gridview making my view controller the delegate for the gridview.
- (void)viewDidLoad
{
[super viewDidLoad];
self.gridView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
self.gridView.autoresizesSubviews = YES;
self.gridView.delegate = self;
self.gridView.dataSource = self;
images=[[NSMutableArray alloc]init];
[images addObject:#"http://t3.gstatic.com/images?q=tbn:ANd9GcTOXAzFMoK441mcn9V0OemVe_dtAuCpGjBkLrv4rffyOjYIo45BEw"];
[self.gridView reloadData];
}
If I set a breakpoint on
[self.gridView reloadData];
the line is executed but reloadData method in AQGridView is not called. The only difference from the ImageDemo is I do not have a .xib file for the view controller. Have I forgotten to hook up something, resulting in the datasource methods not being called?
If there's no XIB, then who's creating the gridView? If it's never created, then it would be NIL, and you'd have the behavior you describe. (If that's it, then just adding:
self.gridview = [AQGridView alloc] initWithFrame: ...]; should suffice.
Had the same problem. Solved by replacing the view with the AQGridView.
[self.view addSubview:self.gridView]
self.view = self.gridView;
Full method:
- (void) viewDidLoad
{
[super viewDidLoad];
self.gridView = [[AQGridView alloc] init];
self.gridView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
self.gridView.autoresizesSubviews = YES;
self.gridView.delegate = self;
self.gridView.dataSource = self;
self.view = self.gridView;
[self.gridView reloadData];
}
Maybe you could try implementing this:
- (void)LoadSearch
{
NSURL *test1 = [NSURL URLWithString:#"http://www.4ddraws.com/search_iphone.asp"];
NSURLRequest *test = [NSURLRequest requestWithURL:test1];
[web4D setScalesPageToFit:(YES)];
[web4D loadRequest:test];
}
My issue is that the back button will not restore its visibility if my web request does not finish before or soon after ViewWillAppear has fired.
I have a navigation based iPhone 4.0 application used a simple Root and Detail view setup.
I am working with data that is returned from a webservice so when I push my detail view in its ViewDidLoad function I call my web service method in a separate thread and the Iphone lifecycle does its thing on the main thread. I must disable/hide the back button until the web request has finished (or failed) so I call self.navigationItem.hidesBackButton = YES; in ViewDidLoad and self.navigationItem.hidesBackButton = NO; in the delegate function which fires once my web request has finished or failed.
I already tried the following:
[self.navigationItem performSelectorOnMainThread:#selector(setHidesBackButton:) withObject:NO waitUntilDone:NO];
[self.navigationItem setHidesBackButton:NO];
[self.view setNeedsDisplay];
[self.navigationController.view setNeedsDisplay];
UINavigationItem *nav = self.navigationItem;
nav.hidesBackButton = NO;
Root View Controller Push Code:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
ArticleViewController *articleViewController = [[ArticleViewController alloc] initWithNibName:#"ArticleViewController" bundle:nil];
NewsArticle *newsArticle = [newsItems objectAtIndex:indexPath.row];
articleViewController.articleID = newsArticle.newsID;
[self.navigationController pushViewController:articleViewController animated:YES];
[newsArticle release];
[articleViewController release];
}
Details View Controller Code:
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.hidesBackButton = YES;
id scrollView = [[[self webContent] subviews] objectAtIndex:0];
if([scrollView respondsToSelector:#selector(setBackgroundColor:)] )
{
[scrollView performSelector:#selector(setBackgroundColor:)
withObject:[UIColor blackColor]];
}
[self getNewsArticle];
}
//Fires when the web request has finished
- (void) finish:(NewsArticle *)newsArticleFromSvc {
self.navigationItem.hidesBackButton = NO;
self.newsArticle = newsArticleFromSvc;
[self bindNewsArtice];
}
Any help is GREATLY appreciated I can hardly ##$&^ believe that hiding a button in a UI could cause me this much wasted time.
Try use this method of UINavigationItem :
- (void)setHidesBackButton:(BOOL)hidesBackButton animated:(BOOL)animated
I wasn't able to solve this problem. Instead I tweaked my App Logic to make hiding he back button not necessary.
Every time I load a new page with UIWebView the before loaded page is shown for a short time.
How can I clear that cache? Another possibility would be to dealloc UIWebview. I tried that but than my UIWebView is always "empty". How should the alloc and dealloc be done in this case?
I noticed that the UIWebView is consuming about 10 MB RAM. Now the UIWebView is loaded together with the ViewController. And the view is autoreleased as well as the UIWebView is autoreleased. Wouldn't it be better to dealloc the WebView each time?
Solution:
- (void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
CGRect frame = CGRectMake(0, 0, 320, 480);
self.webView = [[[UIWebView alloc]initWithFrame:frame] autorelease];
self.webView.scalesPageToFit = YES;
[self.view addSubview:self.webView];
}
- (void) viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[self.webView removeFromSuperview];
self.webView = nil;
}
I had nearly the same problem. I wanted the webview cache to be cleared, because everytime i reload a local webpage in an UIWebView, the old one is shown. So I found a solution by simply setting the cachePolicy property of the request. Use a NSMutableURLRequest to set this property. With all that everything works fine with reloading the UIWebView.
NSURL *url = [NSURL fileURLWithPath:MyHTMLFilePath];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData];
[self.webView loadRequest:request];
Hope that helps!
My solution to this problem was to create the UIWebView programmatically on viewWillAppear: and to release it on viewDidDisappear:, like this:
- (void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.webView = [[[UIWebView alloc]initWithFrame:exampleFrame] autorelease];
self.webView.scalesPageToFit = YES;
self.webView.delegate = self;
self.webView.autoresizingMask = webViewBed.autoresizingMask;
[exampleView addSubview:self.webView];
}
- (void) viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[self.webView removeFromSuperview];
self.webView.delegate = nil;
self.webView = nil;
}
If you do this and your UIWebView doesn't get released you should check it's retain count and understand who is retaining it.
If you want to remove all cached responses, you may also want to try something like this:
[[NSURLCache sharedURLCache] removeAllCachedResponses];
If I had to make a guess your property for webView is
#property (nonatomic, retain) IBOutlet UIWebView *webView;
Correct? The important part is the retain.
self.webView = [[UIWebView alloc]initWithFrame:frame];
The above code allocates a UIWebView (meaning retainCount = 1) and then adds it to self.webView, where it is retained again (retainCount = 2).
Change the code to the following, and your webView should be deallocated.
self.webView = [[[UIWebView alloc]initWithFrame:frame] autorelease];
addSubview: can only be called on UIViews.
//[self.navigationController addSubview:self.webView];
should be
[self.navigationController.view addSubview:self.webView];
I did something simpler, I just set the webView's alpha to 0 on loadRequest and 1 on webViewDidFinishLoad.
thewebview.alpha=0.0;
thewebview.backgroundColor=[UIColor whiteColor];
[thewebview loadRequest:[NSURLRequest requestWithURL:url(str)]];
and
- (void)webViewDidFinishLoad:(UIWebView *)webView {
thewebview.alpha=1.0;
}
(Actually I have a fade-in, as rendering f.ex. a PDF can take 1/5 seconds or so.)
You could also load #"about:blank" after the webview disappears, then it will be empty the next time you access it.