iKy1e | iOS Developer

Tutorial: Customise UIWebView Menu

In safari if you tap and hold down over an image you get a list of options which includes things not in the UIWebView’s standard popup menu, like saving the image.

Now the UIWebView doesn’t provide a way to customise it’s menus by default so we have to turn to some custom code and javascript to override it’s default behaviour.


Tutorial

NOTE: Code only tested on iPhone but should in theory work on iPad too if the actionSheet is displayed using the -(void)showFromRect:(CGRect)rect method and use use a bit more javascript to find out what the link, or images dimensions are.

Apple has disabled this feature (among others) in UIWebViews and kept it for Safari only.

However you can recreate this yourself by extending this tutorial, Customize The Contextual Menu Of UIWebView (which is what we are doing now).

 

Once you’ve finished this tutorial you’ll want to add a few extra’s so you can actually save images (which the tutorial doesn’t cover). I added an extra notification called @”tapAndHoldShortNotification” after 0.3 seconds which calls a method with just the disable callout code in it (to prevent both the default and your own menu popping while the page is still loading, a little bug fix).

 

Also to detect images you’ll need to extend the JSTools.js, here’s mine with an extra function.

 

    function MyAppGetHTMLElementsAtPoint(x,y) {
    	var tags = ",";
    	var e = document.elementFromPoint(x,y);
    	while (e) {
    		if (e.tagName) {
    			tags += e.tagName + ',';
    		}
    		e = e.parentNode;
    	}
    	return tags;
    }
    
    function MyAppGetLinkSRCAtPoint(x,y) {
    	var tags = "";
    	var e = document.elementFromPoint(x,y);
    	while (e) {
    		if (e.src) {
    			tags += e.src;
    			break;
    		}
    		e = e.parentNode;
    	}
    	return tags;
    }
    
    function MyAppGetLinkHREFAtPoint(x,y) {
    	var tags = "";
    	var e = document.elementFromPoint(x,y);
    	while (e) {
    		if (e.href) {
    			tags += e.href;
    			break;
    		}
    		e = e.parentNode;
    	}
    	return tags;
    }

Now you can detect the user clicking on images and actually find out the images url they are clicking on, but we need to change the -(void)openContextualMenuAtPoint: method to provide extra options.

 

Again here’s mine (I tried to copy Safari’s behaviour for this):

    - (void)openContextualMenuAt:(CGPoint)pt{
    	// Load the JavaScript code from the Resources and inject it into the web page
    	NSString *path = [[NSBundle mainBundle] pathForResource:@"JSTools" ofType:@"js"];
    	NSString *jsCode = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    	[webView stringByEvaluatingJavaScriptFromString:jsCode];
    
    	// get the Tags at the touch location
    	NSString *tags = [webView stringByEvaluatingJavaScriptFromString:
    					  [NSString stringWithFormat:@"MyAppGetHTMLElementsAtPoint(%i,%i);",(NSInteger)pt.x,(NSInteger)pt.y]];
    
    	NSString *tagsHREF = [webView stringByEvaluatingJavaScriptFromString:
    						  [NSString stringWithFormat:@"MyAppGetLinkHREFAtPoint(%i,%i);",(NSInteger)pt.x,(NSInteger)pt.y]];
    
    	NSString *tagsSRC = [webView stringByEvaluatingJavaScriptFromString:
    						 [NSString stringWithFormat:@"MyAppGetLinkSRCAtPoint(%i,%i);",(NSInteger)pt.x,(NSInteger)pt.y]];
    
    
    
    	UIActionSheet *sheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];
    
    	selectedLinkURL = @"";
    	selectedImageURL = @"";
    
    	// If an image was touched, add image-related buttons.
    	if ([tags rangeOfString:@",IMG,"].location != NSNotFound) {
    		selectedImageURL = tagsSRC;
    
    		if (sheet.title == nil) {
    			sheet.title = tagsSRC;
    		}
    
    		[sheet addButtonWithTitle:@"Save Image"];
    		[sheet addButtonWithTitle:@"Copy Image"];
    	}
    	// If a link is pressed add image buttons.
    	if ([tags rangeOfString:@",A,"].location != NSNotFound){
    		selectedLinkURL = tagsHREF;
    
    		sheet.title = tagsHREF;
    		[sheet addButtonWithTitle:@"Open"];
    		[sheet addButtonWithTitle:@"Copy"];
    	}
    
    	if (sheet.numberOfButtons > 0) {
    		[sheet addButtonWithTitle:@"Cancel"];
    		sheet.cancelButtonIndex = (sheet.numberOfButtons-1);
    		[sheet showInView:webView];
    	}
    	[selectedLinkURL retain];
    	[selectedImageURL retain];
    	[sheet release];
    }

 

NOTES: ‘selectedLinkURL’ and ‘selectedImageURL’ are NSString’s declared in the .h file to let them be accessed throughout the class, for saving or opening the link later.

 

So far we’ve just been going back over the tutorials code making changes but now we will move into what the tutorial doesn’t cover (it stops before actually mentioning how to handle saving the images or opening the links).

 

To handle the users choice we now need to add the actionSheet:clickedButtonAtIndex: method.

 

    -(void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex{
    	if ([[actionSheet buttonTitleAtIndex:buttonIndex] isEqualToString:@"Open"]){
    		[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:selectedLinkURL]]];
    	}
    	else if ([[actionSheet buttonTitleAtIndex:buttonIndex] isEqualToString:@"Copy"]){
    		[[UIPasteboard generalPasteboard] setString:selectedLinkURL];
    	}
    	else if ([[actionSheet buttonTitleAtIndex:buttonIndex] isEqualToString:@"Copy Image"]){
    		[[UIPasteboard generalPasteboard] setString:selectedImageURL];
    	}
    	else if ([[actionSheet buttonTitleAtIndex:buttonIndex] isEqualToString:@"Save Image"]){
    		NSOperationQueue *queue = [NSOperationQueue new];
    		NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(saveImageURL:) object:selectedImageURL];
    		[queue addOperation:operation];
    		[operation release];
    	}
    }

This checks what the user wants to do and handles /most/ of them, only the “save image” operation needs another method to handle that. For the progress I used MBProgressHub. Add an MBProgressHUB *progressHud; to the interface declaration in the .h and set it up in the init method (of whatever class you’re handling the webview from).

    progressHud = [[MBProgressHUD alloc] initWithView:self.view];
    	progressHud.customView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Tick.png"]] autorelease];
    	progressHud.opacity = 0.8;
    	[self.view addSubview:progressHud];
    	[progressHud hide:NO];
    	progressHud.userInteractionEnabled = NO;

And the -(void)saveImageURL:(NSString*)url; method will actually save it to the image library. (A better way would be to do the download through an NSURLRequest and update the progress hud in MBProgressHUDModeDeterminate to deflect how long it’ll actually take to download, but this is a more hacked together implementation then that)

    -(void)saveImageURL:(NSString*)url{
    	[self performSelectorOnMainThread:@selector(showStartSaveAlert) withObject:nil waitUntilDone:YES];
    	UIImageWriteToSavedPhotosAlbum([UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:url]]], nil, nil, nil);
    	[self performSelectorOnMainThread:@selector(showFinishedSaveAlert) withObject:nil waitUntilDone:YES];
    }
    -(void)showStartSaveAlert{
    	progressHud.mode = MBProgressHUDModeIndeterminate;
    	progressHud.labelText = @"Saving Image...";
    	[progressHud show:YES];
    }
    -(void)showFinishedSaveAlert{
    	// Set custom view mode
    	progressHud.mode = MBProgressHUDModeCustomView;
    	progressHud.labelText = @"Completed";
    	[progressHud performSelector:@selector(hide:) withObject:[NSNumber numberWithBool:YES] afterDelay:0.5];
    }

And of cause add [progressHud release]; to the dealloc method.

 

 

Hopefully this shows you how to add some of the options to a webView that apple left out. Of cause though you can add more things to this like a “Read Later” option for sending the link to instapaper or a “Open In Safari” button. (looking at the length of this post I’m seeing why the original tutorial left out the finial implementation details)


  1. metalkin reblogged this from iky1e
  2. iky1e posted this
To Tumblr, Love PixelUnion