August 14, 2008

Calling Objective-C from JavaScript in an iPhone UIWebView

Posted in iPhone development tagged , , , , , , , , , , , at 6:47 am by tetontech

Apple now has a new web view class, WKWebView. It is much improved. See this new posting for how to interact with Objective-C from JavaScript and this post for interacting with Swift. The classes and methods are the same for both Swift and Objective-C. (June 13, 2014).

 

The information and source code covered in this posting is included, updated, and made more reliable in the downloadable framework called QuickConnectiPhone. Take a look at this wiki page to learn more more about the built in capabilities and features of this framework. These include GPS, acceleration, and much, much more functionality available from within JavaScript.

The framework uses the approach described below in the original posting but has many features, optimizations, and defect fixes wrapped up for you and ready to use.

 

At long last a methodology has been discovered that will allow calls to be made to objective-c code from the UIWebView. It isn’t as strait forward as I would like but it does work. The example shown here uses it to activate the core location libraries as well as to send messages to NSLog. We all know how hard it can be to find the reasons for JavaScript failures in the UIWebview. This will allow you to do it.

The methodolog uses the UIWebViewDelegate method

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType

The post contains an updated version of code from a previous blog post here at Teton Technical. It includes changes that will soon be in the next version of the QuickConnectiPhone framework.

In order to use this methodology a button with an onclick listener has been defined in the index.html file as seen here.

It tells the UIWebView to load a new page called ‘call’ and passes two parameters, a command and a parameter that can be used by the objective-c. In this case it is actually not used but is here to show you how parameters are passed.

Here is the code fragment that contains the objective-c method that is triggered when a location change is requested,the full code for the class containing this method is at the end of this posting. It checks to see if ‘index.html’ is being called.
If it is then it returns YES so that the load will continue. Any other request for a page changes is stopped by returning NO.
It also parses the URL that is the request and retrieves the cmd and any other parameters sent in the URL.
If you were using the QuickConnectOC, Objective-C, library in on the objective-c side of your application you could simply pass the command and parameter array to it and let it handle it for you.
If you are not using the QuickConnectOC library you will need to handle it yourself with conditional statements. Since I have not yet posted the QCOC library, it should be available this week, I have shown how to embed the conditionals statements for doing logging of messages and calling core location functionallity.

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
NSString *url = [[request URL] absoluteString];

NSArray *urlArray = [url componentsSeparatedByString:@"?"];
NSString *cmd = @"";
NSMutableArray *paramsToPass = nil;
if([urlArray count] > 1){
NSString *paramsString = [urlArray objectAtIndex:1];
NSArray *urlParamsArray = [paramsString componentsSeparatedByString:@"&"];
cmd = [[[urlParamsArray objectAtIndex:0] componentsSeparatedByString:@"="] objectAtIndex:1];
int numCommands = [urlParamsArray count];
paramsToPass = [[NSMutableArray alloc] initWithCapacity:numCommands-1];
for(int i = 1; i < numCommands; i++){
NSString *aParam = [[[urlParamsArray objectAtIndex:i] componentsSeparatedByString:@"="] objectAtIndex:1];

 

[paramsToPass addObject:aParam];
}
}
/*
* if you are using QuickConnectOC within the Objective-C portion of your iPhone application
* you can make the call that is commented out below.
*/
//[[QuickConnect getInstance] handleRequest:cmd withParameters:paramsToPass];
/*
* if you are not using QuickConnectOC you will need to use conditional statements like if-then-else or case statements
* like the example below. You would then use the other parameters passed with the command to make decistions and
* execute code.
*/
if([cmd compare:@"getLocation"] == NSOrderedSame){
[locationManager startUpdatingLocation];
}
else if([cmd compare:@"logMessage"] == NSOrderedSame){
//NSString *message = [[paramsToPass objectAtIndex:0] stringByReplacingOccurrencesOfString:@"%20" withString:@" "];
NSString *message = [[paramsToPass objectAtIndex:0] stringByReplacingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
NSLog(@"JSMessage: %@",message);
}
/*
* Only load the page if it is the initial index.html file
*/
NSRange aSubStringRange = [url rangeOfString:@"index.html"];
if(aSubStringRange.length != 0){
return YES;
}
else{
return NO;
}
}

The JavaScript function, GPSLocation, in index.html that gets called by objective-c is shown below. It shows you how to send messages to be logged by NSLog. This technique should allow you to put any number of debug statements in your JavaScript code and have them displayed in Xcodes run log. This code can be found in the QCUtilities.js file in the soon to be released QCiPhone beta 5 framework.
// the location function call made from the underlying Objective-C framework when the location of the device is determined
function GPSLocation(longitude, latitude, altitude){
window.location = "call?cmd=logMessage&msg="+document.getElementById('messages').innerText;
try{

 

document.getElementById('messages').innerText = 'Longitude: '+longitude+' \nLatitude: '+latitude+'\nAltitude: '+altitude;
return 'It worked!';
}
catch(err)
{
txt="'There was an error on this page.\n\n";
txt+="Error description: " + err.description + "'";
window.location = "call?cmd=logMessage&msg="+txt;
}
return 'It failed';
}

There is no guarantee that these messages will be logged prior to the function shown here returning. In fact it has not happened for me yet. Here is the log file results. You can see that the JavaScript log message appears after the return value of the GPSLocation function has been logged by the method that called it.

2008-08-14 00:05:46.638 RoadRunner2[38029:20b] Location updated
2008-08-14 00:05:46.639 RoadRunner2[38029:20b] GPSLocation(37.331689, -122.030731, 0.000000);
2008-08-14 00:05:46.642 RoadRunner2[38029:20b] done with result: ‘It worked!’
2008-08-14 00:05:46.647 RoadRunner2[38029:20b] JSMessage: Put Messages Here

I hope this helps you all out.

Here is the full code for the updated UIWebView example.
/*
Copyright 2008 Lee S. Barney

This file is part of QuickConnectiPhoneHybrid.

QuickConnectAJAX is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

QuickConnectAJAX is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with QuickConnectAJAX. If not, see .

*/
//the acceleration function call that is made from the underlying Objective-C framework when the device experiences acceleration
function accelerate(x, y, z){
/*
document.getElementById(‘messages’).innerHTML = ‘got acceleration’;
document.getElementById(‘messages’).innerHTML += ‘
session value: ‘+session.getAttribute(‘curAccel’);
*/
var accelObject = session.getAttribute(‘curAccel’);
//document.getElementById(‘message’).innerHTML += ‘
xValue: ‘+accelObject.x+’ new x: ‘+x;
accelObject.x = x;
accelObject.y = y;
accelObject.z = z;

handleRequest(‘accel’);

return true;
}
//an object to hold acceleration values in all three dimensions.
function AccelerationObject(){
this.x = 0;
this.y = 0;
this.z = 0;
}

// the location function call made from the underlying Objective-C framework when the location of the device is determined
function GPSLocation(longitude, latitude, altitude){
window.location = “call?cmd=logMessage&msg=”+document.getElementById(‘messages’).innerText;
try{

document.getElementById(‘messages’).innerText = ‘Longitude: ‘+longitude+’ \nLatitude: ‘+latitude+’\nAltitude: ‘+altitude;
return ‘It worked!’;
}
catch(err)
{
txt=”‘There was an error on this page.\n\n”;
txt+=”Error description: ” + err.description + “‘”;
window.location = “call?cmd=logMessage&msg=”+txt;
}
return ‘It failed’;
}
//this function will scroll the current view by the x and y amount. This function is ususally called by the Objective-C portion of an application.
function customScroll(xAmount, yAmount){
//document.getElementByID(‘message’).innerHTML = xAmount+” “+yAmount+”
“;
window.scrollBy(xAmount,yAmount);
return ‘done’;
}

//remove white space from the beginning and end of a string
function trim(aString){
return aString.replace(/^\s+|\s+$/g, ”);
}

//replace all occurances of a string with another
function replaceAll(aString, replacedString, newSubString){
while(aString.indexOf(replacedString) > -1){
aString = aString.replace(replacedString, newSubString);
}
return aString;
}
//stop an event from doing its’ default behavior since it will be handled in the BCO and VCO
function stopDefault(event){
if(event){
if (event.preventDefault)// non-IE
event.preventDefault();
event.returnValue = false;// IE
}
}
/*
* These mapping functions require functions for the business rules,
* view controls, error controls, and security controls NOT the names
* of these items as strings.
*/
function mapCommandToBCF(aCmd, aBRule){
var aMappingBean = new Array();
aMappingBean[‘bcf’] = aBRule;
businessMap[aCmd] = aMappingBean;
}
function mapCommandToVCF(aCmd, aVCF){
var aMappingBean = viewMap[aCmd];
if(aMappingBean == null){
aMappingBean = new Array();
aMappingBean[‘viewFunctionArray’] = new Array();
viewMap[aCmd] = aMappingBean;
}
var funcArray = aMappingBean[‘viewFunctionArray’];
funcArray.push(aVCF);
}
function mapCommandToECF(anErrorCmd, anECF){
var aMappingBean = new Array();
aMappingBean[‘ecf’] = anECF;
errorMap[anErrorCmd] = aMappingBean;
}
function mapCommandSCF(aCmd, aSCF){
var aMappingBean = securityMap[aCmd];
if(aMappingBean == null){
aMappingBean = new Array();
aMappingBean[‘securityFunctionArray’] = new Array();
securityMap[aCmd] = aMappingBean;
}
var funcArray = aMappingBean[‘securityFunctionArray’];
funcArray.push(aSCF);
}

function mapCommandToValCF(aCmd, aValCF){
var aMappingBean = validationMap[aCmd];
if(aMappingBean == null){
aMappingBean = new Array();
aMappingBean[‘validationFunctionArray’] = new Array();
validationMap[aCmd] = aMappingBean;
}
aMappingBean[‘validationFunctionArray’].push(aValCF);
}

 

function debug(msg){
document.getElementById('debug').innerHTML = msg;
}

41 Comments »

  1. […] sample, source, UIWebView at 5:22 pm by tetontech A newer version of this code example can be found here. Please look through this example before you go to the new one. The new one covers how to call […]

  2. Laurent said,

    Nice work!

  3. Rob Ellis said,

    Your timing is impeccable, we started a project 2 weeks ago out of iPhoneDevCamp II that does this exactly. Check out Phonegap.com

    Cheers Rob

  4. tetontech said,

    Rob,

    Sounds like Phonegap does some of the things that QuickConnectiPhone does. Have you considered using it as the implementation platform for Phonegap since it is already written and has some hardening behind it?

  5. tetontech said,

    Rob,

    I have briefly checked out your proposed API for the image functionality. I don’t see how you will be getting an image back from the getImage() call. All calls to JavaScript will be analogous to asynchronous calls.

    You may want to rethink how that works.

    I am putting QuickConnectOC up on the sourceForge site. You may want to look at that as a solution for handling the requests for behavior on the OC side.

  6. Jay Crossler said,

    Great stuff – I implemented a Google map interface with this using the rotation, location, and other ObjC variables. Took a bit to wrap my mind around, but I love the simplicity and ingenuity of your approach. Even better is using the call?cmd structure to send commands out from the browser to ObjC. Not the ideal method, but it worked enough for my uses.

    Great stuff, keep it coming!

  7. […] iPhone, JavaScript, multi-threaded, Objective-C, UIWebView at 4:00 am by tetontech In a previous post I showed a methodology for calling Objective-c from JavaScript using a window.location call. While […]

  8. tetontech said,

    QuickConnectiPhone 1.0 is now available. It is a framework that uses the technique described above and has many other capabilities. Look at this https://tetontech.wordpress.com/2008/10/30/quickconnectiphone-10-available/ posting to see more.

  9. Idan said,

    Hi Guys,
    Would you know if its possible to access a JavaScript Database (sqlite, html5) from the native SDK?

    Thanks in advance.

  10. tetontech said,

    Idan,

    You can access both the SQLite db in the UIWebView as well as any SQLite db you ship with your application. There is a file in version 1.1.1 of QuickConnect called DataAccessObjects.js That is a wrapper designed to help with this.

    Methods:
    (Browser Based)
    getData(sql, preparedStatementParams)
    setData(sql, preparedStatementParams)\

    (Native DB’s)
    getNativeData(sql, preparedStatementParams)
    setNativeData(sql, preparedStatementParams)

    You must understand that all calls to the databases on iPhones are asynchronous. If you roll your own access and try to do it like you would on a backend (synchronously) you will be very frustrated.

  11. idan said,

    Thanks for the response.
    Would you know if the other way around is also possible. (i.e. To access the DB created in the javascript code using the native SDK calls.)

  12. tetontech said,

    You certainly can. I know you could have Objective-C code make a call to JavaScript and have the JavaScript retrieve the data. Since this retrieval would be asynchronous the JavaScript callback function would have to make a call back down to the Objective-C to deliver the data.

    Is it possible to make an Objective-C call directly to the database used by the UIWebView? Maybe. It would have to be discovered where this file is and what it’s name is.

    Depending on what you are trying to do, it may be easier just to use the wrapper provided by QuickConnectiPhone and put the data directly into the native db. It isn’t any harder than putting it in the UIWebView db.

  13. idan said,

    Thanks for your response , great info.

  14. idan said,

    Thanks again.
    Can I use this library for a commercial use?

    Idan.

    • tetontech said,

      You bet. I chose the LGPL license for this very reason.

  15. SJ said,

    Hi,

    On iphone if I load a local html file, I can not store any cookies. It works fine in safari on mac. However it fails on the iphone.

    Most of the APIs from javascript to objective-C and back seem to be asynchronous. Is there a synchronous alternative to simulate cookies from javascript (especially when using the get call. Set cookie can be asynchronous).

    Any help is appreciated.

    Thanks and Regards,

    SJ

    • tetontech said,

      SJ,

      The issue as I see it is this:

      Cookies are only sent back to the origination location of the original page loaded. If the original page is located on the device then cookies are not sent back to some other server when an AJAX call is made.

      If I understand your problem this is what you are trying to do:
      load a local file
      make an AJAX call make an additional AJAX call that requires cookies from the original AJAX call

      Is this the case?

      I started working on this problem on Thursday of last week but as of yet have not been able to come up with a solution that keeps the initial page locally on the machine. I will keep looking at a solution for this.

      The framework’s direct JavaScript calls to the underlying Objective-C code is asynchronous because Apple did not give us the ability to make synchronous calls. We can only make a URL location change request in JavaScript and then capture that on the Objective-C side. If you look at the makeCall method in the com.js file you will see this happening.

      The QC framework linearizes these calls for you if you use the mapCommandToBCF, mapCommandToVCF, and the other mapCommandTo* functions. In other words you don’t need to worry about making sure that one call to the underlying Objective-C completes before another is made. The framework will take care of it for you.

      Lee

  16. […] Now, this is something that I haven’t coded myself because I haven’t run into a situation where I was using a web app that needed to add functionality not already implemented within Phonegap when working with Iphone web app code. However, the author of QuickConnect has an excellent tutorial on doing this here. […]

  17. sandrar said,

    Hi! I was surfing and found your blog post… nice! I love your blog. 🙂 Cheers! Sandra. R.

    • tetontech said,

      Thanks Sandra. Glad to be of help.

      Lee

  18. Luca said,

    I’m not sire I’ve understood how the circle closes, but I’m having many problems, due to the fact that I don’t have the function load() called in

    Omitting this function, takes to a request passed to the shouldStartLoadWithRequest which is equal to my page absolute path without any parameter or “call?” string.

    What does load() function do?
    Could you explain me how all this works?

    Thanks in advance

    Luca

    • tetontech said,

      Luca

      I’m not sure from your explanation what the issue is. The load() function is the body onload event listener.

      The basics of how this works are:
      1. Each time the location of the page is changed, either by a click on a link or by updating it using JavaScript, the shouldStartLoadWithRequest Objective-C method is called and passed what ever URL you have built and used.
      2. In this method you have access to the entire URL associated with the link or the requested change.
      3. You can then parse the URL to determine what you want to do.
      4. the shouldStartLoadWithRequest always returns NO so that you don’t actually go to some other page. Neither will the UIWebView update when NO is returned.
      5. Based on your parsing of the URL you can now execute some Objective-C code.
      6. After executing your objective-c code you can then use another OC method to call a JavaScript function and pass data back to the JavaScript in your page. Remember, these calls are asynchronous.

      Lee

  19. Raj said,

    Hi Lee,

    I am one of the user of Quickconnect.It reduces development time.I use more web stuff in iphone application development.Recently,I found a tool which explore all the file inside app bundle.So,my html and javascript files get exposed .So,I want to protect them.Do you know any way to resolve this issue?
    I am thinking of going into encryption or keep all javascript source into objective-c file as string.Because,objective-c source and .NIB cant be exposed from .app folder.

    Thanks

    • tetontech said,

      Any application for any platform/language can be taken apart. Even those written in Objective-C can be decompiled and the source viewed. The xib files are just XML and can be viewed as well.

      There are JavaScript obfuscation tools available. I’ve never used one but they are available.

  20. Raj said,

    Lee,

    Thanks for your Quick reply.

    I recently came across obfuscation tools.Its nothing but removing the space.Simply,hard to read format.
    Objective-c can be decompiled?oh,I didnt know it.I will check it.

    I tried to open compiled .NIB files.But I couldn’t.If you can ,kindly let me know the way to read compiled .NIB file.

    Raj

  21. […] Calling Objective-C from JavaScript in an iPhone UIWebView August 2008 25 comments 3 […]

  22. mike said,

    Hey
    I went through your post and must say its good.
    I was just wondering, about the following scenario:
    I have made a UIWebview for login on my website. I have retrieved and stored the associated cookies, and now as soon as I log in I want my application to get back to the view of my iphone and display the mobile app. like there should be a direct call from uiwebview to the home page(made in xcode, only if the user is authneticated). Is it possible to do the same somehow using cookies or is there any other simpler mechanism to go from uiwebview back to the files on your iphone that you have made using xcode.

    Thanks in advance

    • tetontech said,

      Mike,

      I’m not sure what you meant by “go from uiwebview back to the files on your iphone that you have made using xcode”.

      Lee

      • mike said,

        I meant, I have a log in function on my website, where the username and userid are authenticated
        Now, as soon as they are authenticated in my uiwebview(website), i want my application to get out of uiwebview and display the transactions.xib interface i made in xcode, aka, i just am using uiwebview for authentication of user name and password, rest of the functions are purely objective c based and dont need uiwebview.
        So i was wondering if it is possible for me to implement a function somewhere in my webview did finish load thing so as to retrieve my transactions.xib file ? more like an interaction between uiwebview and objective c

      • tetontech said,

        It is possible to load the xib file you have available and begin processing. It may be much easier to use the NSURLConnection class to begin with rather than the UIWebView. That way you would stay in Objective-C for the entire span of your application.

        There are many examples on the web of using NSURLConnection. If you need one I can provide one.

        Lee

  23. mike said,

    An NSURLConnection object provides support to perform the loading of a URL request.

    I have the webview working, so I do not need to refer any url, I just want to load the xib file without referring to url, nsurlconnection has nothing to do with this.

    I will need something like NSUserDefaults , if you have any clue about nsuserdefaults with respect to uiwebview to retrieve usernames, let me know, along with that, getting back to the previous topic, also if you have any clue about retriving a native objective c xib/or implementation file upon authentiacation of the webview content, let me know.
    i appreciate it

    Thanks

  24. mona said,

    Hey, I just wanted to ask if there is a way to access several html files at once, to do run a search algorithm on all the html files. thx in advance

  25. […] Calling Obj-C from javascript: calling objective-c from javascript […]

  26. […] Calling Objective-C from JavaScript in an iPhone UIWebView […]

  27. […] Calling Objective-C from JavaScript in an iPhone UIWebView […]

  28. […] Calling Obj-C from javascript: calling objective-c from javascript […]

  29. […] Calling Obj-C from javascript: calling objective-c from javascript […]

  30. […] Calling Obj-C from javascript: calling objective-c from javascript […]


Leave a reply to SJ Cancel reply