Archive
Adding Swiping to UIWebView using JavaScript (iOS4/iOS5) and custom URL scheme
Hey folks, welcome to my new Tutorial, which will cover the topic of how to implement swiping in a UIWebView using JavaScript and will explain how to send variables back to our UIWebView using Custom URL Schemes.
The code itself is straightforward but will cover a lot of cool things.
To load the JavaScript code we call in our webViewHasFinishedLoading Delegate method the following code:
#define DEVICE_IOS5() (([[[UIDevice currentDevice] systemVersion] floatValue]>=5.0))
if (DEVICE_IOS5()){
[self.webView stringByEvaluatingJavaScriptFromString:@"loadListenerIOS5()"];
} else {
[self.webView stringByEvaluatingJavaScriptFromString:@"loadListenerIOS4()"];
}
This is the JavaScript code:
File: SwipeEvent.js
Use your local html to let the browser know about it. I showed how to do that in my last tutorial.
EDIT: One more thing. When copying JavaScript files from the Finder to XCode, it will not treat it as an added resource but as a file to compile. Of course that is not possible so it will give you warnings back and the file will not be copied to the resource folder on your device / simulator. To fix that problem, go to target->build settings and than drag and drop the file from Compile Sources to Copy Bundle Resources.
var startX;
var startY;
var diffX;
var diffY;
var hypo;
var isIOS5;
function loadListenerIOS4(){
var body = document.getElementsByTagName("body")[0];
body.addEventListener("touchstart", touchStart, false);
body.addEventListener("touchend", touchEnd, false);
body.addEventListener("touchmove", touchMove, false);
isIOS5=0;
return "JS Listener iOS4 loaded";
}
function loadListenerIOS5(){
var body = document.getElementsByTagName("body")[0];
body.addEventListener("touchstart", touchStart, false);
body.addEventListener("touchend", touchEnd, false);
body.addEventListener("touchmove", touchMove, false);
isIOS5=1;
return "JS Listener iOS5 loaded";
}
function fireSwipeScheme(a,b,c){
var scheme="swipe:" + a + ":" + b + ":" + c;
document.location=scheme;
}
function touchStart(event){
diffX=0;
diffY=0;
if (isIOS5){
startX=event.pageX;
startY=event.pageY;
} else {
startX=event.touches[0].pageX;
startY=event.touches[0].pageY;
}
}
function touchEnd(event){
if (diffX>50||diffX<-50){
fireSwipeScheme(diffX,diffY,hypo);
}
}
function touchMove(event) {
var a;
var b;
if (isIOS5){
a = event.pageX - startX;
b = event.pageY - startY;
} else {
a = event.touches[0].pageX - startX;
b = event.touches[0].pageY - startY;
}
var c = calcHypotenuse(a,b);
diffX=a;
diffY=b;
hypo=c;
}
function calcHypotenuse(a,b){
return Math.sqrt(a*a,b*b);
}
LoadListenerIOS4 and LoadListeneriOS5 are there to attach an event listener to the body object of our DOM. The first is a constant which can be either “touchstart”, “touchend”, “touchmove” or “touchcancel” and the second determines the function we are about to call. So in this case, “touchstart” calls touchStart( … ).
Both loaders are the same, only that one sets the variable isIOS5 to 1 and the other sets it to 0. This is important if you look into the touchStart function.
On iOS5, to get the current x/y coordinates of a touch, some must call event.pageX and event.pageY, while on iOS4, its event.touches[0].pageX and event.touches[0].pageY.
The same goes for all the other touch function.
So what we do here is, we register the touch coordinates on touch start, calculate all the way through touchmove the current distance and fire immediately an url scheme to the UIWebView if the touches ended.
fireSwipeScheme is also pretty interesting.
The variable “scheme” is a combination about the keyword “swipe” and a, b, c which are actually the variables diff x, diff y and the hypotenuse.
So, how do we now get these variables into our objective C Code?
In the delegate method webViewShouldStartLoadOnRequest: withNavigationType: we add the following code
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
NSString*scheme=[[request URL]absoluteString];
//NSLog(@"received scheme: %@", scheme);
NSArray *components=[scheme componentsSeparatedByString:@":"];
if ([[components objectAtIndex:0]isEqualToString:@"swipe"]){
[self handleSwipeInWebView:components];
return NO;
}
return YES;
}
And to our class we add the following method:
#define kMinSwipeDistanceHorizontalInWebView 100.0f
#define kMaxSwipeDistanceVerticalInWebView 50.0f
-(void)handleSwipeInWebView: (NSArray*)code{
@autoreleasepool {
NSInteger swipeDistX=[[NSNumber numberFromString:1]intValue];
NSInteger swipeDistY=abs([[NSNumber numberFromString:1]intValue]);
NSLog(@"swipeDistY: %d", swipeDistY);
NSLog(@"swipeDistX: %d", swipeDistX);
if ((swipeDistX>kMinSwipeDistanceHorizontalInWebView||swipeDistX<-kMinSwipeDistanceHorizontalInWebView)&&swipeDistY>kMaxSwipeDistanceVerticalInWebView){
MasterViewController*ptr=(MasterViewController*)self.masterViewController;
if (ptr){
if (swipeDistX<-kMinSwipeDistanceHorizontalInWebView){
[ptr moveDownInTreeHierarchy];
} else if (swipeDistX>kMinSwipeDistanceHorizontalInWebView){
[ptr moveUpInTreeHierarchy];
}
}
}
}
In the delegate method, the UIWebView catches all clicked links events or all document.location events from JavaScript. By sending a custom URL script we can first return NO, to make sure that no new page will get loaded and we can use it actually as an encoder for our variables. The url scheme in this example looks something like this
Swipe:-40:30:-50.
In the delegate method, I break the array into components by separating them by the ASCII character ‘:’. In the handleSwipeInWebView method, I can easily use the components than to let my code do what I want him to do.
In my case, I am firing the swipe Event from the DetailViewController in my SplitViewController and notify the MasterViewController about it.
UIWebView – Calling Javascript from iOS with passing parameters … Tutorial
Right now I am working on a project for my company which has to display a lot of content, that is delivered in the XML format and consists of small chunks of HTML formatted text-code.
The only way on iOS to display HTML is to use the UIWebView and so I had to write some small Javascript code which can be called to display the content. It has to be dynamic, as the content is sorted by chapters and the user can navigate through a tree structure to display only relevant content.
So what I did was, parsing the XML and than writing everything into Core Data (btw. Core Data is AWSOME!… the speed, the usability.. incredible). With Core Data I do the sorting and when the user opens content, I load my local HTML, which does actually nothing but tell the browser where the .JS files are.
The reason why I write this tutorial is because I couldn’t find a good one on the web that covers that topic which my requirements (.JS files, parameter passing from Objective C to JS)
Btw: this is not finished code , it works but is rough to get the basic system to work.
<html>
<head>
<meta http-equiv=“content-type”content=“text/html;charset=utf-8″ />
<title>DisplayContent</title>
<style>
</– th, td { vertical-align: top; } –>
</style>
<script src=“displayContent.js”></script>
</head>
<body>
</body>
</html>
The JS File looks like this:
function textNodeWithColorAndSize(text, rgb, size, font){
var textNode=document.createTextNode(text);
var font=document.createElement(“font”);
font.style.color=rgb;
font.style.fontSize=size;
font.appendChild(textNode);
return font;
}
function printHeadOfTable(code, headline) {
var body = document.getElementsByTagName(“body”)[0];
var table = document.createElement(“table”);
var tableBody = document.createElement(“tbody”);
var row = document.createElement(“tr”);
for (var i = 0; i < 3; i++) {
var cell = document.createElement(“td”);
var cellText;
if (i==0){
cell.width=40;
cellText=textNodeWithColorAndSize(code, “#FF3344″, “12px”, “font”);
} else if (i==1) {
cellText=textNodeWithColorAndSize(headline, “#000000″, “18px”, “font”);
} else {
cellText=textNodeWithColorAndSize(“[E-Mail Button]“, “#555555″, “12px”, “font”);
}
cell.appendChild(cellText);
row.appendChild(cell);
}
tableBody.appendChild(row);
table.appendChild(tableBody);
table.style.marginLeft=20;
body.appendChild(table);
}
function resize(){
var frames=document.getElementsByTagName(“div”);
for (var i=0;i<frames.length;i++){
frames[i].style.width=“94%”;
}
}
function printBodyOfTable(bodyString){
var body = document.getElementsByTagName(“body”)[0];
var frame=document.createElement(“div”);
frame.innerHTML=bodyString;
frame.style.width=“94%”;
frame.style.marginLeft=20;
body.appendChild(frame);
}
So, fine enough. Resize does resize all elements <div> to 94% of the web view browser. This is important because of rotation.
printBodyToTable( … ) and printHeadToTable ( … ) are functions to print content.
So, when you load up now the webpage nothing appears of course. Thats why we have to call the JS from our Objective C code. This looks like this:
The array assets is passed from the fetchedResults.
-(void)loadWebViewWithAssets: (NSArray **const)assets{
[self setWebViewFrame:CURRENT_ORIENTATION()];
@autoreleasepool {
_assetArray=[[NSArray alloc]initWithArray:*assets];
NSURL *file = [NSURLfileURLWithPath:[[NSBundle mainBundle]pathForResource:@”contentView”ofType:@”html”]];
NSURLRequest *urlRequest=[[NSURLRequest alloc]initWithURL:file];
[self.webView loadRequest:urlRequest];
[urlRequest release];
}
}
So this loads our basic html. Now when the webView is done loading, we are free to call our Javascript functions.
I think it is a bit a pity, that the JS Script methods ONLY work AFTER the web view finished its loading. It would be cool to do that beforehand but well.. lives a b**** sometimes
Code:
(Asset and Content are Core Data entities.)
The abort calls are there for debugging purposes.
-(void)webViewDidFinishLoad:(UIWebView *)webView{
for (int i=0;i<[_assetArray count];i++){
Asset*asset=[_assetArray objectAtIndex:i];
Content*content=asset.content;
if (!content){
dbgAbort();
}
NSString *jsPrintHeadOfTable=[[NSString alloc]initWithFormat:@”printHeadOfTable(\”%@\”, \”%@\”)”, asset.at_code, asset.at_title];
NSString *jsPrintBodyOfTable=[[NSString alloc]initWithFormat:@”printBodyOfTable((\”%@\”))”, content.at_content];
if (!jsPrintBodyOfTable){
dbgAbort();
}
[self.webView stringByEvaluatingJavaScriptFromString:jsPrintHeadOfTable];
[self.webView stringByEvaluatingJavaScriptFromString:jsPrintBodyOfTable];
[jsPrintHeadOfTable release];
[jsPrintBodyOfTable release];
}
[_assetArray release];
}
So, you see, we are even able to pass parameters to our Javascript which is incredible awesome. The same webView method is able as well to retrieve values from the javascript by capturing the NSString return value. But there a lot of tutorials out there for this.
Ah yeah, and is very important that you prepare your parameters. content.at_content for example is a complete HTML formatted text. Before you pass it you have to make sure that there are no _ ” _ characters in it (replace them with _ ‘ _ . Now newline characters, no carriage return etc.. Just to tell you
Took me some while to get the idea.
From this point on we can now call the stringByEvaluatingJavaScriptFromString method wherever and whenever we like.. Cool huh?
Yeah, this was my first tutorial and I hope it helped ..
Greetings
Markus