Building a custom PhoneGap plugin for iOS


August 26, 2013
title

Many of us are used to developing mobile HTML5 applications with various frameworks and have wanted to dive into hybrid application development. Of those who actually create hybrid mobile HTML5 applications, very few of us actually make full use of this flexible application model. In this blog post, I will walk you through the first of a few steps in creating a custom PhoneGap plugin for iOS.

The goal of the following exercise is to enable the web app filesystem access through our custom component built into the PhoneGap wrapper. That way we will be able to use native code through JavaScript and enjoy the best of both worlds.

We’ll develop a class named FileWriter that will obtain a JSON date value from the browser and write it to the iOS file system. If you’ve never coded an ounce of Objective C, don’t worry. I’ll be explaining quite a bit along the way.

This article is designed as a step-by-step process, but if you just want to read the raw code and jump right in, I have the project available for you to download here: https://github.com/ModusCreateOrg/custom-cordova-plugin-blog.

What you’ll need

Before we can break ground, you’re going to need a few things.

  1. An OS X powered computer. (This should go without saying)
  2. Download & install XCode
  3. Download and extract PhoneGap (docs)
  4. Configure Safari to allow remote debugging.

With those things out of the way, we can get down to business.

Steps we’ll take

  1. Create the PhoneGap project
  2. Create the plugin
  3. Implement the plugin via JavaScript method calls

This seems simple enough, right? The first step entails dropping down to a command line shell and running the PhoneGap create function to create the project. The second step is more involved, requiring us to actually create an Objective-C class and then implement it. :)


1. Create the PhoneGap project

To create the PhoneGap project, you’ll need to drop down to your shell. We’ll be using the PhoneGap create command.

Here’s the syntax for the create command.

#Change dir to your project directory
Usage: ~/Downloads/phonegap-2.9.0/lib/ios/bin/create [--shared] [--arc] <path_to_new_project> <package_name> <project_name>
    --shared (optional): Link directly against the shared copy of the CordovaLib instead of a copy of it.
    --arc (optional): Enable ARC.
    <path_to_new_project>: Path to your new Cordova iOS project
    <package_name>: Package name, following reverse-domain style convention
    <project_name>: Project name

To make things easy, I’ve listed the steps below:

#Change dir to your project directory
cd /www/pgplugin

# Create the PhoneGap project:
# I've typed out the path to the PhoneGap extracted files 
# and use the iOS project binaries 
~/Downloads/phonegap-2.9.0/lib/ios/bin/create . CustomPlugin CustomPlugin

You won’t get any output from PhoneGap’s create method, so in order to see what’s in the directory, you’re going to have to look at its contents.

#Look at the directory contents
ls -l
total 0
drwxr-xr-x   7 jgarcia  wheel  238 Aug  8 10:51 CordovaLib
drwxr-xr-x@ 10 jgarcia  wheel  340 Aug  8 10:51 CustomPlugin
drwxr-xr-x@  3 jgarcia  wheel  102 Aug  8 10:51 CustomPlugin.xcodeproj
drwxr-xr-x@  9 jgarcia  wheel  306 Aug  8 10:51 cordova
drwxr-xr-x@ 10 jgarcia  wheel  340 Aug  8 10:51 www

Launching the project for the first time

In order to launch the project, you’re going to need to open the project with XCode. The easiest way to do it at this point is to enter open CustomPlugin.xcodeproj/ in your shell and XCode will automatically launch, opening the project.

xcode_first_time

At this point, you’re going to want to run the project, but the scheme might be set as “iPad 5.0” by default. run_button

The iPad simulator can take up a ton of space and get in our way for this project, so we should change the scheme to iPhone 6.1. To do this, click on the “Set active scheme button”

set active scheme

… and select ‘iPhone 6.1’.

set_active_scheme_2

From here, you can hit the “Run” button, or hit ⌘R on your keyboard and you’ll see the simulator pop up with the welcome screen from PhoneGap

cordova_first_time-2

Our project is now setup and we’re ready to roll into phase two: Building the plugin!


2. Adding the plugin structure to the project

We’re going to build the plugin from the bottom up. That is, we’re going to write some Objective-C code to support JavaScript calls via the PhoneGap bridge.

The first step is to open XCode and click on the Show the Project Navigator button: show_project_nav_btn

Next, expand the CustomPlugin project and click on “config.xml”. This is going to open up the configuration file that we’ll need to edit to tell PhoneGap that we’re adding a new feature (plugin) to the project.

features1

Next, scroll down in the editor to the end of the file and add a new line at line under the closing </feature> tag at line 112:

features2

Inject the following XML…

<!-- My custom plugin -->
<feature name="FileWriter">
    <param name="ios-package" value="FileWriter" />
</feature>

… So that config.xml now looks like the following: features3

Next, we’ll create the class that will be responsible to listening to the calls from PhoneGap’s JavaScript bridge. To do this expand the Classes, right click on it and choose New File.

new_file

From there, a window drawer will appear, requesting what type of file you’d like to create. Choose Cocoa Touch and Objective-C class menu items and press Next.

new_file2

From there, focus on the Class name input field, enter FileWriter and press next. Then select the Subclass of input field and enter CDVPlugin. new_file3

Click on Create to actually create the your class’s header and class file. new_file4

You should be able to see that the FileWriter.h and FileWriter.m files in the file explorer.
new_file5

Why two files? Well, in short Objective-C class definitions are split into two files. The header file (.h) and the class file itself (.m). The header file is used to describe the class, while the class file itself contains the actual code for the class. We’ll have to edit both files to create the custom plugin. If you want to read more on the Objective-C programming language, check out this article from Apple.

All right. We’re ready to start writing some Objective-C!

Creating the plugin code

The first step is to edit the FileWriter.h header file and replace the entire file contents with the following:

#import <Cordova/CDV.h>

@interface FileWriter : CDVPlugin

// This will return the file contents in a JSON object via the getFileContents utility method
- (void) cordovaGetFileContents:(CDVInvokedUrlCommand *)command;

// This will accept a String and call setFileContents to persist the String on to disk
- (void) cordovaSetFileContents:(CDVInvokedUrlCommand *)command;

#pragma mark - Util_Methods

// Pure native code to persist data
- (void) setFileContents;

// Native code to load data from disk and return the String.
- (NSString *) getFileContents;

@end

The code above is broken up into two main sections: 1) the interface methods that we’ll be calling from the PhoneGap JavaScript bridge, which are prefixed with cordova, and 2) utility methods that will be used internally by the interface methods.

Note: I tend to prefix method names with cordova so that I know that they will be called via the JS bridge, but it’s not required by any means. Also, I split up functionality between the interface methods and methods that will do the work. The reason being you can move reusable work to instance level methods, such as writing contents to a file. Also, this is a good way to demonstrate how we can do native Objective C work within the Cordova plugin framework.

The next step is to fill in the code for FileWriter.m. I’ve added comments to the code to make it a relatively easy read.

@implementation FileWriter 
- (void) cordovaGetFileContents:(CDVInvokedUrlCommand *)command {
    
    // Retrieve the date String from the file via a utility method
    NSString *dateStr = [self getFileContents];
    
    // Create an object that will be serialized into a JSON object.
    // This object contains the date String contents and a success property.
    NSDictionary *jsonObj = [ [NSDictionary alloc]
                               initWithObjectsAndKeys :
                                 dateStr, @"dateStr",
                                 @"true", @"success",
                                 nil
                            ];
    
    // Create an instance of CDVPluginResult, with an OK status code.
    // Set the return message as the Dictionary object (jsonObj)...
    // ... to be serialized as JSON in the browser
    CDVPluginResult *pluginResult = [ CDVPluginResult
                                      resultWithStatus    : CDVCommandStatus_OK
                                      messageAsDictionary : jsonObj
                                    ];
    
    // Execute sendPluginResult on this plugin's commandDelegate, passing in the ...
    // ... instance of CDVPluginResult
    [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}

- (void) cordovaSetFileContents:(CDVInvokedUrlCommand *)command {
    // Retrieve the JavaScript-created date String from the CDVInvokedUrlCommand instance.
    // When we implement the JavaScript caller to this function, we'll see how we'll
    // pass an array (command.arguments), which will contain a single String.
    NSString *dateStr = [command.arguments objectAtIndex:0];

    // We call our setFileContents utility method, passing in the date String
    // retrieved from the command.arguments array.
    [self setFileContents: dateStr];
    
    // Create an object with a simple success property.
    NSDictionary *jsonObj = [ [NSDictionary alloc]
                               initWithObjectsAndKeys : 
                                  @"true", @"success",
                                  nil
                            ];
       
    CDVPluginResult *pluginResult = [ CDVPluginResult
                                      resultWithStatus    : CDVCommandStatus_OK
                                      messageAsDictionary : jsonObj
                                    ];
    
    [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}

#pragma mark - Util_Methods
// Dives into the file system and writes the file contents.
// Notice fileContents as the first argument, which is of type NSString
- (void) setFileContents:(NSString *)fileContents {

    // Create an array of directory Paths, to allow us to get the documents directory 
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    
    // The documents directory is the first item
    NSString *documentsDirectory = [paths objectAtIndex:0];

    // Create a string that prepends the documents directory path to a text file name
    // using NSString's stringWithFormat method.
    NSString *fileName = [NSString stringWithFormat:@"%@/myTextFile.txt", documentsDirectory];
    
    // Here we save contents to disk by executing the writeToFile method of 
    // the fileContents String, which is the first argument to this function.
    [fileContents writeToFile : fileName
                  atomically  : NO
                  encoding    : NSStringEncodingConversionAllowLossy
                  error       : nil];
}

//Dives into the file system and returns contents of the file
- (NSString *) getFileContents{

    // These next three lines should be familiar to you.
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    
    NSString *documentsDirectory = [paths objectAtIndex:0];

    NSString *fileName = [NSString stringWithFormat:@"%@/myTextFile.txt", documentsDirectory];
    
    // Allocate a string and initialize it with the contents of the file via
    // the initWithContentsOfFile instance method.
    NSString *fileContents = [[NSString alloc]
                               initWithContentsOfFile : fileName
                               usedEncoding           : nil
                               error                  : nil
                             ];

    // Return the file contents String to the caller (cordovaGetFileContents)
    return fileContents;
}

@end

In the last bit, we’ll have to write some HTML and JavaScript to allow us to interface with our Objective-C code.


3. Adding the supporting HTML

Open the index.html file located in www/ of the project with XCode or your editor of choice.

Locate the following block:

<div class="app">
    <h1>Apache Cordova</h1>
    <div id="deviceready" class="blink">
        <p class="event listening">Connecting to Device</p>
        <p class="event received">Device is Ready</p>
    </div>
</div>

… and replace it with:

<div class="app">
    <div id="fileContentsDiv">...</div>
    <button id='getFileContents'>GET file contents</button>
    <button id='setFileContents'>SET file contents</button>
</div>

What that’s going to do is allow us to have two buttons to tap on providing means to persist and retrieve data as well as a container to display the data obtained from the file. You can see what the HTML edits look like by hitting ⌘R on your keyboard to launch the application.

edit_html

Lastly, we’ll have to inject the JavaScript that will tie the buttons in with the PhoneGap calls.

Writing the JavaScript

Before we add the JavaScript to close things out, I want you to take a glance at the pattern for calling PhoneGap plugin methods:

cordova.exec(
    callbackFn,     // A callback function that deals with the JSON object from the CDVPluginResult instance
    errorFn,        // An error handler
    'TargetClass',  // What class to target messages to (method calls = message in ObjC)
    'methodToCall', // Which method to call
    [ 'array', 'of', 'arguments'] // These go in the CDVInvokedUrlCommand instance's.arguments property
);

To complete this project, we’ll need to open up www/js/index.js and locate the onDeviceReady method. Replace it with the following code. I’ve commented it so you can read line by line what I’m doing and where things line up with the FileWriter Objective-C class we just wrote.

onDeviceReady: function() {
    var contentsDiv    = document.getElementById('fileContentsDiv'),
        getContentsBtn = document.getElementById('getFileContents'),
        setContentsBtn = document.getElementById('setFileContents');
    
    //Set file contents
    setContentsBtn.addEventListener('click', function() {
        
        // Create a Date string. It will look something like: "2013-08-13T22:04:58.811Z"
        var dateStr = new Date().toJSON();
        
        // Ask cordova to execute a method on our FileWriter class
        cordova.exec(
            // Register the callback handler
            function callback(data) {
                contentsDiv.innerHTML = 'File contents set.';
                console.log('Wrote date ' + dateStr);
            },
            // Register the errorHandler
            function errorHandler(err) {
                alert('Error');
            },
            // Define what class to route messages to
            'FileWriter',
            // Execute this method on the above class
            'cordovaSetFileContents',
            // An array containing one String (our newly created Date String).
            [ dateStr ]
        );

    });
    
    //Get file contents
    getContentsBtn.addEventListener('click', function() {
               
        cordova.exec(
            function callback(data) {
                // data comes from the NSDictionary instance (jsonObj) from our Objective C code.
                // Take a look at the cordovaGetFileContents method from FileWriter.m and you'll see
                // where we add dateStr as a property to that Dictionary object.
                var msg = 'Current file contents: <br />' + data.dateStr;
                contentsDiv.innerHTML = msg;
            },
            function errorHandler(err) {
                alert('Error');
            },
            'FileWriter',
            'cordovaGetFileContents',
            [  ]
        );

    });
},

We can see that the JavaScript is pretty straightforward. We register click event handlers on the buttons, which invoke the cordova* methods we defined in our FileWriter class. The final step in this process is to get this bad boy to work!


Run the project by hitting ⌘R on your keyboard. You’ll be greeted by the project running.

run_project

From there, click SET file contents and then GET file contents. What you’ll see is that the UI updates accordingly and you’ll see the date time stamp that was written to disk.

run_project2

But in order to really see if something is on disk, we’re going to have to go digging a little bit. We’ll need to find out where the app files are located in your computer. To do this, hit ⇧⌘C in XCode to reveal the Console & Log. Take a look at the log, and you’ll see something similar to the following:

app_log

The two lines that are most important in this case are Finishing load of:... and Wrote date .... The first Finishing load line comes from PhoneGap, and tells you that the index.html file was located successfully by the UIWebView instance. To find the text file, we’ll need to navigate to ~/Library/Application Support/iPhone Simulator/6.1/Applications in Finder, enter the folder that matches the application ID that you see in the log. There, you’ll see the CustomPlugin compiled app and a Documents directory.

If you expand the Documents directory, you’ll see myTextFile.txt. docs dir

Go ahead and edit that file. You’ll see that it contains the JSON date value. file_contents1

Change the file and save your edits. Then press GET file contents in your app. You’ll see that the file contents are read by FileWriter and displayed.

file_contents2

There you go! Everything works as designed.

A quick word about app security

In case you’re worried about app security, don’t fret. What we did was purely for a great learning experience. All applications in iOS are sandboxed, meaning they only have access to their “container” directory. This article from Apple explains it very well.

Closing

We spent a lot of time going over how to create a simple PhoneGap iOS plugin that hooks into native Objective-C functionality to write files to the file system. I hope you had as much fun following this article as I had developing the code for and writing it.

See it live

This post is a summary of my ModUX talk on Building Custom PhoneGap Plugins for iOS. If you can be in Amsterdam on September 20th 2013, then I’d love to hear about your PhoneGap experiences.

Edit: For those who missed ModUX, you can view my talk here.

Please share

Do you think this article would be useful to your friends? Please share your thoughts, comments, and questions in the comment box below. Thanks for reading! 😀



jay copy
Jay Garcia is co-founder and managing director at Modus Create. He is a U.S. Air Force veteran with 20 plus years of technology and consulting experience in leading RIA development for companies around the world. He is co-organizer of the NoVa.JS and NYC.JS meetups, and is actively involved in the software communities that provide business frameworks and technologies, which enable rich mobile and desktop web experiences.

  • Dina Youssef

    Thanks for the tutorial, it was helpful in getting me to finish my email plugin for iOS

    • Ishmeet Kalsi

      Hi Dina i have a project which needs the file in pdf and attach the pdf to email client of ios to send.

      Can u help on this..Really appreciated.

      Using phone gap 3.0+

  • mvp

    well said, thank u…

  • Milind Desai

    FileWriter.m needs #import “FileWriter.h” in the beginning

  • Jhon Jaiver Lopez Calderon

    I was looking for this for a while! Thanks.

  • Pingback: Adding plugins to Phonegap (3.1.0) Cleaver app not workingCopyQuery CopyQuery | Question & Answer Tool for your Technical Queries,CopyQuery, ejjuit, query, copyquery, copyquery.com, android doubt, ios question, sql query, sqlite query, nodejsquery, dn()

  • http://hatimonline.com/ Shahzada Hatim

    thank you, great write up. Worked like a charm.

  • Gavin Conway

    Very helpful write-up. One detail: If I am not mistaken, “- (void) setFileContents;” in FileWrite.h should read: “- (void) setFileContents:(NSString *)fileContents;”

    • Yasin Abdul

      @Gavin : You are right. Thats a silly mistake. And you can delete that part from .h also. it has no meaning in .h file.

  • Phil Merrell

    This was a great walkthrough on building a Phonegap plugin. Using this as a reference I built CanOpen, a phonegap plugin for detecting if native apps are installed in iOS. https://github.com/philbot5000/CanOpen. Thanks!

  • Vote 539

    Thank you for the great tutorial! With your guidance I was able to make my first working PhoneGap plugin.

    I’ve written another tutorial for PhoneGap and iOS with a focus on the camera API. I hope someone finds it helpful! http://codrspace.com/vote539/writing-a-custom-camera-plugin-for-phonegap/

    • aaron_n_smith

      Approve—
      Sent from Mailbox for iPhone

  • Oren

    Thanks

    • aaron_n_smith

      Approve—
      Sent from Mailbox for iPhone

  • http://www.mobilepundits.com/ mobilepundits

    Very helpful write up! With your guidelines i was able to create custom phone Gap plugin for iOS and waiting your next article to PhoneGap plugin for Android platform.

  • Yasin Abdul

    Its my first day . And i must say that i am confident for writing plugins for iOS. Its a very good and self explanatory tutorial. Thanks guys.

  • ishwardgret

    Great tutorial really appreciate ur concern!!!

  • Sujit Kadam

    i got 1 eroor

    ERROR: Plugin ‘FileWriter’ not found, or is not a CDVPlugin. Check your plugin mapping in config.xml
    [CDVCommandQueue executePending] [Line 158] FAILED pluginJSON = [

    “FileWriter6800906”,

    “FileWriter”,

    “cordovaGetFileContents”,

    [

    ]

    ]

    And this is my config.xml

    • arist1213

      yes, i got the same problem.

      ERROR: Plugin ‘FileWriter’ not found, or is not a CDVPlugin. Check your plugin mapping in config.xml.

    • arist1213

      i found is a type error with FileWrite(r), check it closely.

  • Karl Shifflett

    Need some help with editing and testing my plugins.

    I need to create several Cordova plugins. I’ve looked at many example plugins on github. I understand the end result code required to publish a plugin.

    I understand how to use plugman to create the plugin scaffolding.

    I understand how to create cordova applications using the cordova iOS-shell.

    What I just can’t figure out is how to use Xcode to edit the plugin I created.

    Where do I copy the created plugin folder to? Xcode requires an xcode project so the project created by the iOS-shell seems like the project I need to use.

    How do I include the plugin folder into xcode project? Which folder does it get copied to?

    I’ve read every post I can find for the last 4 days, but just can’t figure this out.

    Thank you very much for the guidance.

    Karl

  • dm33

    Is this using deprecated config.xml?? When I look at current documentation, the config.xml file for plugins always uses the tag. Why is this using ?

  • dm33

    When trying to run this using phone gap, getting error message, ERROR: Plugin ‘FileWriter’ not found, or is not a CDVPlugin. Check your plugin mapping in config.xml.


What We Do

We’ll work closely with your team to instill Lean practices for ideation, strategy, design and delivery — practices that are adaptable to every part of your business.

See what Modus can do for you.

LET'S GET STARTED

We're Hiring!

Join our awesome team of dedicated engineers.

Loading...

APPLY NOW