Memory problems with UIWebViews - iphone

I have a webviewcontroller, which I create one instance of, and then I change the request, and show the webview based on which row is selected in a tableview.
// Replace tableView:didSelectRowAtIndexPath with the following
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath
{
CGRect rect = self.view.frame;
CGFloat width = [UIScreen mainScreen].bounds.size.width;
CGFloat height = [UIScreen mainScreen].bounds.size.height;
rect.size.width = width; //self.view.frame.size.width;
rect.size.height = height; //self.view.frame.size.height;
RSSEntry *entry = [_allEntries objectAtIndex:indexPath.row];
page = [[WebViewController alloc] initWithUrl:entry.articleUrl];
page.view.frame = rect;
[self.view addSubview:page.view];
}
My problem is, that after loading a few of these, I get memory warnings and crash.
In another tab, I also have a webview which loads a google map when the tab loads. This crashes the app almost as soon as it is loaded.
Since I am only making one instance of each, I don't see why this problem is happening. Previously I created a new webviewcontroller each time, and then released it, but I had the same problem.
Here is the code to load the Google map:
#interface MapViewController : UIViewController {
IBOutlet UIWebView *mapPage;
}
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
url_string = #"http://maps.google.fr/maps/ms?msa=0&msid=213119412551786463007.0004b3fb7f4c123377402&z=12";
url = [NSURL URLWithString:url_string];
request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url_string]];
[request setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData];
mapPage.scalesPageToFit = YES;
mapPage.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
[mapPage loadRequest:request];
[super viewDidLoad];
}
The UIWebView is created in interface builder.
It runs out of memory and crashes right after the map is finished loading, or as soon as you try to interact with the map. It appears to be the very same problem as above, but this time there are no tabs, or no changing the request that the UIWebView uses.

Each time when you select row in UITableView you create a new object of WebViewController and add its view (for some reason?) to current controller's view. I'm not sure that you at least remove that view from superview (you also have to release WebViewController). And of course it will take more and more memory.
And what is the reason to create a whole UIViewController and use it as subview? Why you didn't use UINavigationController?

It turns out the pages were being cached.
In the applicationDidReceiveMemoryWarning method of my application, I added a line to clear the cache. This fixed both of my p
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
/*
Free up as much memory as possible by purging cached data objects that can be recreated (or reloaded from disk) later.
*/
[[NSURLCache sharedURLCache] removeAllCachedResponses];
}

Related

How to update a progressbar during instantiation of timeconsuming ui elements?

I would like to update a progressbar while i am instantiating some ui elements that takes some times. I first create my view during the viewLoad method and add my progress bar there. Once my view appeared in the viewDidAppear method I am making several uikit objects instantiation but i would like to update the progress bar in the mean time. I am not sure how to proceed since everything should happen in the main thread as it is ui elements.
Here is part of my code:
-(void) viewDidAppear:(BOOL)animated
{
// precompute the source and destination view screenshots for the custom segue
self.sourceScreenshotView = [[UIImageView alloc] initWithImage:[self.view pw_imageSnapshot]];
[self.progressBar setProgress:.3];
SCLViewController *rvc = [[SCLViewController alloc] init];
UIView *destinationView = rvc.view;
destinationView.frame = CGRectMake(0, 0, kWidthLandscape, kHeightLandscape);
self.destinationScreenshotView = [[UIImageView alloc] initWithImage:[destinationView pw_imageSnapshot]];
[self.progressBar setProgress:.5];
}
In the above code I just need to create two screenshots of views to use them later on. The problem is that i only see the last update (.5) when setting the progress to the progress bar. What is the proper way to do this update?
You can use the performSelectorInBackground:withObject: method in order to instantiate your heavy views. That method (the one that instantiates your views) will have to set your progress bar progress in the main thread.
So your code would look something like this:
- (void)viewDidAppear:(BOOL)animated
{
[self performSelectorInBackground:#selector(instantiateHeavyViews) withObject:nil];
}
- (void)instantiateHeavyViews
{
self.sourceScreenshotView = [[UIImageView alloc] initWithImage:[self.view pw_imageSnapshot]];
[self performSelectorOnMainThread:#selector(updateMyProgressView:) withObject:[NSNumber numberWithFloat:0.3f] waitUntilDone:YES];
SCLViewController *rvc = [[SCLViewController alloc] init];
UIView *destinationView = rvc.view;
destinationView.frame = CGRectMake(0, 0, kWidthLandscape, kHeightLandscape);
self.destinationScreenshotView = [[UIImageView alloc] initWithImage:[destinationView pw_imageSnapshot]];
[self performSelectorOnMainThread:#selector(updateMyProgressView:) withObject:[NSNumber numberWithFloat:0.5f] waitUntilDone:YES];
}
- (void)updateMyProgressView:(NSNumber *)progress
{
[self.progressBar setProgress:[progress floatValue]];
}
Edit: of course, it won't animate your progress bar (I don't know if that is what you wanted). If you want it to move on while your views are being created, you should use a delegate to be notified of the progress, and this can be a bit harder. This way you would be able to update the progress bar every time the delegate is notified.

Memory Management, ARC - what to nil?

Background -
I am using automatic reference counting on a project. The root view is a Table View (Master / Detail setup) showing a list of "slide shows". Click on a table cell and you are taken to the detail view which consists of a Scroll view with views (viewController.view) in it (this is the "slide show"). Each slide show has a front cover and back cover (same view controller formatted differently) that sandwich an variable number of pages. Here is the code to load the slide show:
- (void)loadScrollView
{
// The front and back cover are set in Interface Builder because they
// are reused for every slide show, just labels are changed.
[self.scrollView addSubview:self.frontCoverViewController.view];
[self.frontCoverViewController setCoverTitle:_data.name creationDate:_data.creationDate isFrontCover:YES];
[self.pagesArray addObject:self.frontCoverViewController];
for (int i = 0; i < [self getTotalNumberOfComps]; i++)
{
PageViewController *pageView = [[PageViewController alloc] init];
pageView.data = [_compsArray objectAtIndex:i];
[_scrollView addSubview:pageView.view];
pageView.data.imgView = pageView.imageView;
pageView.slideShowViewController = self;
[_pagesArray addObject:pageView];
}
[self.scrollView addSubview:self.backCoverViewController.view];
[self.backCoverViewController setCoverTitle:_data.name creationDate:_data.creationDate isFrontCover:NO];
[self.pagesArray addObject:self.backCoverViewController];
[self.scrollView bringSubviewToFront:_frontCoverViewController.view];
[self setCurrentPage:0];
}
Problem -
So Im trying to reuse this slide show view controller so I need to nil and recreate the pages in the middle because each slide show has a different number of slides. Note a slide [PageViewController] is just a view with an ImageView in it. It has more functionality so we need the controller however the main display of the V.C. is the ImageView. I have created the following method to "empty" the slide show before running loadScrollView again with new data. Here is the empty method:
- (void)saflyEmptyScrollView
{
for (int i = 0; i < [self.pagesArray count]; i++)
{
if (i == 0 && i == ([self.pagesArray count]-1)) {
CoverViewController *cover = (CoverViewController*)[self.pagesArray objectAtIndex:i];
[cover.view removeFromSuperview];
} else {
PageViewController *page = (PageViewController*)[self.pagesArray objectAtIndex:i];
[page.view removeFromSuperview];
page = nil;
}
}
self.pagesArray = nil;
self.pagesArray = [[NSMutableArray alloc] init];
}
Big Question -
My main question is do I need to set the ImageView of each of these pages to nil? Or does setting the page itself to nil also free up the memory used by the ImageView/Labels/etc that are used in that view controller?
I tried adding self.imageView = nil; to the PageViewController's viewDidUnload and viewWillUnload methods (one at a time not in both) and I realized that setting page = nil does not call the pages Unload methods. Am I freeing up memory correctly.
I've read a lot of articles but Im still not sure if Im managing memory in the best way possible. Thanks so much for the help!
Generally, you shouldn't have to set things to nil. And in this specific case, the setting things to nil is doing nothing.
The line page = nil; is redundant, because the variable page goes out of scope immediately afterwards anyway. ARC knows this and doesn't need you to set it to nil.
And self.pagesArray = nil; is redundant because you follow it with self.pagesArray = [[NSMutableArray alloc] init];. The second line on its own will suffice.

UISegmented Controller to load one view with a few Youtube UIWebViews and another view with Images

Sorry for the long question, but I have been stuck on this for days and have exhausted all other help.
Currently, I have a tab bar application with four tabs. In the second tab (SecondViewController), I have a segmented controller at the top that should switch between "videos" and "images". The videos page should have around 5 youtube videos loaded in UIWebView using the code here. The images view should contain around 5 thumbnails that, when clicked on, open into a larger picture. My problem is that I have tried out many different ways of accomplishing this, and none seem to work to any extent. Really the main thing I am looking for here is the recommended way of going about switching between two views using a segmented controller and if it is possible to load the views from different files (videosView.h/m and imagesView.h/m).
In SecondViewController.m, I have the app respond to the UISegmentedController using the following, though I have absolutely no idea if this is even close to correct.
- (IBAction)segmentedControlChanged
{
switch (segmentedControl.selectedSegmentIndex)
{case 0:
[self.view addSubview:videosView.view];
[imagesView.view removeFromSuperview];
NSLog(#"1");
break;
case 1:
[self.view addSubview:imagesView.view];
[videosView.view removeFromSuperview];
NSLog(#"2");
break;
default:
break;
}
}
In videosView.h, I only have the following:
#import <UIKit/UIKit.h>
#interface videosView : UIWebView
{
}
- (videosView *)initWithStringAsURL:(NSString *)urlString frame:(CGRect)frame;
#end
In videosView.m, I have the following, though I am getting a warning on the initWithFrame line.
- (videosView *)initWithStringAsURL:(NSString *)urlString frame:(CGRect)frame;
{
if (self = [super init])
{
// Create webview with requested frame size
self = [[UIWebView alloc] initWithFrame:frame];
// HTML to embed YouTube video
NSString *youTubeVideoHTML = #"<html><head>\
<body style=\"margin:0\">\
<embed id=\"yt\" src=\"%#\" type=\"application/x-shockwave-flash\" \
width=\"%0.0f\" height=\"%0.0f\"></embed>\
</body></html>";
// Populate HTML with the URL and requested frame size
NSString *html = [NSString stringWithFormat:youTubeVideoHTML, urlString, frame.size.width, frame.size.height];
// Load the html into the webview
[self loadHTMLString:html baseURL:nil];
}
return self;
}
#end
imagesView is made, but has no added code it in currently, as I am just trying to get the videos sorted out first.
My recommendation:
Use one view controller and have the view controller contain both views. You are already doing that.
You can still use separate files (subclasses of UIView).
Do not use addSubview: and removeFromSuperView:, but rather set these "container" view as hidden as appropriate.
Also, in the segmentedControlChanged method, do all the other necessary switching tasks, such as canceling open URL connections etc.
Do the initialization of the web content of the container views in viewDidLoad rather than in the initializer. Make sure you do not freeze the UI but use asynchronous loading.
EDIT: Adding subclassing code.
in SecondViewController.h:
#include VideosView.h
...
#property (nonatomic, retain) VideosView *videoView;
in SecondViewController.m
-(void)viewDidLoad {
self.videosView = [[VideosView alloc] init];
// add to superview etc.
}
Whenever you want to execute view specific code, just call any method you define in VideosView.h and implement in .m.
[self.videosView playVideo];

UITableViewCell holding a UIWebViewCell with Dynamic Height and a Loading Animation

I am currently trying to nest a UIWebView (among other elements) into a custom UITableViewCell subclass.
I need to dynamically determine the size of the cell (using heightForRowAtIndexPath) but for some reason when I do it like so, the height always gets printed as 0:
UIWebView* wv = [[[UIWebView alloc] init] autorelease];
[wv loadHTMLString:body baseURL:nil];
[wv setNeedsLayout]; //I don't think these are necessary but I tried anyway.
[wv setNeedsDisplay];
CGSize wvSize = [wv sizeThatFits:webViewBounds];
NSLog(#"WVHEIGHT %f", wvSize.height); //THIS IS WHERE IT PRINTS.
CGFloat retVal = 10.0f;
retVal += 50 > wvSize.height ? 50 : wvSize.height;
retVal += 2 + 15 + 10;
return retVal;
Now, if I do that exact same calculation using the cell.webView that I have access to in cellForRowAtIndexPath, it returns a non-zero value. However, I'd really like to avoid loading an entire cell just to figure out how tall it should be...
Additionally, UIWebView is very slow at rendering text (and it is unfortunately not negotiable that I use UIWebView), so I was wondering if there's a way to tell it to display the typical Apple "rotating gear" activity icon until it has its text fully loaded and ready to render.
I would recommend hiding each UIWebView until it is done loading, and showing a loading indicator instead. I would then make a dictionary with keys that match the tags, and for each entry in the dictionary, I would store the size of the UIWebView.
Make your controller conform to the UIWebViewDelegate protocol, and use the following code:
- (void)webViewDidFinishLoad:(UIWebView *)webView {
NSString *output = [webView stringByEvaluatingJavaScriptFromString:#"document.getElementById('this').offsetHeight;"];
[self.webViewHeights setValue:[output floatValue] forKey:[NSString stringWithFormat:#"%f", webView.tag]];
}
After that, add code to hide your loading view and display your UIWebView, then call a table reload. In your cell height method, just check the correct array element for your UIWebView height.
Edit:
It seems I left out the important part!
OK, so, to create non-visible UIWebViews, you just need to create them, but not actually add them to a view. For example, you could do something like this in your viewDidLoad method:
UIWebView *newWebView = [[UIWebView alloc] initWithFrame:newFrame];
[newWebView loadHTMLString:body baseURL:nil];
newWebView.tag = 1;
[self.webViewDictionary setObject:newWebView forKey:#"1"];
Then in your table code, say something like
NSString *tagString = [NSString stringWithFormat:#"%d", indexPath.row];
UIWebView *curWebView = [self.webViewDictionary objectForKey:tagString];
if (curWebView.loading == NO) {
CGRect newFrame = curWebView.frame;
newFrame.size.height = [self.webViewHeights objectForKey:tagString];
curWebView.frame = newFrame;
[cell addSubview:curWebView];
} else {
UIActivityIndicatorView *activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorStyleGray];
[cell addSubview:activityIndicator];
}
I don't think sizeThatFits: is what you want to use.
sizeThatFits:
Asks the view to calculate and return
the size that best fits its subviews.
Your webview does not have any subviews, only HTML.
UIWebView has no init method of its own, so you probably should use initWithFrame: (from the UIView superclass) to set the size directly. You can set it to the size you want, which is the size of the tableView cell in this case.

UIImageView weird image property issue

I'm dealing with a super easy task that somehow introducing some difficulties...
All I'm trying to do is to create a view controller and set its UIImageView's image property to some image.
When I try to that, I get nil =\
GenericViewController *genericViewController = [[GenericViewController alloc] init];
UIImage *image = [UIImage imageNamed:#"Camera.png"];
genericViewController.genericImageView.image = image;
NSLog(#"%#", genericViewController.genericImageView.image);
Output: (null)
I imagine genericImageView is set up either in a nib or in the -loadView method. However, at the point in which you're trying to access the image view, the view for the VC hasn't been loaded yet. The quick fix is to call
(void)genericViewController.view;
before accessing genericImageView. This will force the view to load. The "better" approach would be to give genericViewController an image property that you assign to, then in its setter you can say
- (void)setGenericImage:(UIImage *)image {
if (_genericImage != image) {
[_genericImage release];
_genericImage = [image retain];
if ([self isViewLoaded]) {
self.genericImageView.image = image;
}
}
}
and in -viewDidLoad you can say
- (void)viewDidLoad {
[super viewDidLoad];
self.genericImageView.image = self.genericImage;
}
This method, besides being more modular and architecturally-sound, also has the advantage where if the view controller's view is unloaded (say, another view is pushed onto the nav stack and a memory warning comes along), when it gets re-loaded it will still have the image.