アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

Cordova(PhoneGap)のプラグインの作り方

こんにちは、橋本です。

今日はCordova(PhoneGap)のプラグインの作り方について書いていきたいと思います。

Cordovaは、HTML, CSS, Javascriptを組み合わせて、iOSAndroidのネイティブアプリが作れるというものです。
Cordovaで用意されたJavascriptAPIを用いることで、ネイティブの機能を使用することができるようになっています。

「HTML、CSSJavascriptでネイティブアプリが作れるとは!ネイティブの機能も使えるし!これはお手軽!!素晴らしい!!!」

と、思うかもしれませんが、Cordovaの内部では、ネイティブのWebviewの上でHTMLを動かし、JavascriptAPIを通じて予め用意されたネイティブの機能を使っているだけなので、実際にアプリを作り始めると、痒いところに手が届かない場面がしばしばあります。

たとえば、カメラを起動して写真や動画を撮ることはできるのですが、カメラ自体の機能を拡張したり、見た目を変えたりといったことは出来ません。
また、iTunesライブラリへのアクセスや、Bluetoothを使うといったことも、APIが用意されていないので出来ません。

ただ、Cordovaにはプラグインを組み込む機構が用意されているため、Objective-CJavaを使ってネイティブコードでプラグインを作ることで、機能を追加することができるようになっています。


というわけで、今回はiOSの場合を例にプラグインの作り方をご紹介したいと思います。


プラグインを作成するためには、ネイティブのコードと、それをjavascriptから実行するためのjavascriptコードを記述する必要があります。

1. ネイティブコードを作成する
というわけで、まずCordovaプロジェクトのPluginsフォルダの中に、Objective-Cのclassファイルを作成します。
このクラスは、CDVPluginクラスの子クラスとして作成してください。



新規ファイルを作成したら、まずは「.h」ファイルを開いてください。
完成後のソースはこんな感じです。

PGMyPlugin.h


#import <Cordova/CDV.h>

@interface PGMyPlugin : CDVPlugin

@property (nonatomic, copy) NSString *callbackId;

- (void)hello:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options;
@end


まず、初期状態ではがインポートされているかと思いますが、これをに修正します。

次に、NSString型のcallbackIdというプロパティを定義します。
これは、javascript側から渡されてくるIDを格納しておくためのプロパティで、javascriptに処理の実行結果を返すときに必要になります。
(プロパティ名はcallbackIdでなくても構いません。)

次に、メソッドを定義しています。
今回は「- (void)hello:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options;」というメソッドを定義しました。
argumentsとoptionsはどちらも、javascriptから渡される引数なのですが、javascript側から渡した引数の配列の中で、オブジェクトはoptionsに連想配列として渡され、文字列や数値の場合には配列に値が入ってきます。

では、次に「.m」ファイルを開いてメソッドを実装していきたいと思います。
完成後のソースはこんな感じです。

PGMyPlugin.m


#import "PGMyPlugin.h"

@implementation PGMyPlugin
@synthesize callbackId;

- (void)hello:(NSMutableArray *)arguments withDict:(NSMutableDictionary *)options
{
    self.callbackId = [arguments pop];
    
    NSString *name = [arguments objectAtIndex:0];
    CDVPluginResult *result;
    NSString *ret;
    
    if (![name isEqual:@""]) {
        NSString *str = [NSString stringWithFormat:@"Hello %@", [arguments objectAtIndex:0]];
        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:str];
        ret = [result toSuccessCallbackString:self.callbackId];
    } else {
        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"名前をください!"];
        ret = [result toErrorCallbackString:self.callbackId];
    }
    
    [self writeJavascript:ret];
}
@end


まず、optionsの一つ目の値としてコールバック用のIDが格納されているので、それをcallbackIdプロパティに格納しています。

次にjavascriptから渡される引数をname変数に格納し、その有無で処理を切り分けています。
CDVPluginResultクラスは、処理結果を作成するためのクラスで、処理結果とステーテスコードを渡して作成します。
また、CDVPluginResultのtoSuccessCallbackStringメソッドや、toErrorCallbackStringメソッドを使うことで、javascriptで定義したコールバック関数を叩くための文字列を作成することができます。
これらのメソッドには、javascriptから渡されたコールバックIDを指定する必要があります。

最後に、writeJavascriptメソッドで、作成した結果の文字列をJavascriptとして実行して完了です。

プラグインの作成が完了したら、このプラグインを実際に使用可能にするために、「Cordova.plist」に登録する必要があります。
具体的には、Cordova.plistのPluginsというDictionaryの中にキーと値を記述します。
キーには、javascript側から呼ぶための名前を記述し、値にはプラグインのクラス名を記述します。
今回は、キーに「jp.co.asial.hello」と記述し、値に「PGMyPlugin」と記述しました。



ネイティブ側はこれで完了です。

2. Javascriptを作成する

次に、プラグインを呼ぶためのjavascriptコードを書いていきます。
完成後のソースはこんな感じです。


<!DOCTYPE HTML>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title></title>
    <script type="text/javascript" charset="utf-8" src="cordova-1.5.0.js"></script>
</head>
<body>
    <input id="input" type="text" />
    <button id="btn">表示</button>
    <script>
        document.addEventListener('deviceready', function(){
            document.querySelector('#btn').addEventListener('click', function(){
                var str = document.querySelector('#input').value;     
                Cordova.exec(function(ret){alert(ret);}, function(error){alert(error);}, 'jp.co.asial.hello', 'hello', [str]);
            });
        });
    </script>
</body>
</html>


javascript側からネイティブのプラグインを呼ぶためには、Cordova.exec()メソッドを使用します。
メソッドの引数は、1つ目から順に、成功時のコールバック関数、失敗時のコールバック関数、Cordova.plistの「キー」に記述した値、呼びたいプラグインのメソッド名、引数の配列となります。

では、実行してみます。



このように、プラグインが実行されていることが確認できるかと思います。

今回は、この簡単なサンプルだけだと物足りないかと思い、iTunesMusicLibraryから音楽を取得するプラグインを作成してみましたので、こっちもよろしければ見てみてください。

PGiTunesMusicLibrary.h


#import <Cordova/CDV.h>
#import <MediaPlayer/MediaPlayer.h>
#import <AVFoundation/AVFoundation.h>

@interface PGiTunesMusicLibrary : CDVPlugin <MPMediaPickerControllerDelegate>

@property (nonatomic, copy) NSString *callbackId;

- (void)showMusicPicker:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options;
@end


PGiTunesMusicLibrary.m


#import "PGiTunesMusicLibrary.h"


@implementation PGiTunesMusicLibrary
@synthesize callbackId;

- (void)showMusicPicker:(NSMutableArray *)arguments withDict:(NSMutableDictionary *)options
{
    self.callbackId = [arguments pop];
    
    // メディアアイテムピッカーの表示
    MPMediaPickerController *picker = [[MPMediaPickerController alloc] initWithMediaTypes:MPMediaTypeAnyAudio];
    
    
    [picker setDelegate:self];
    [picker setAllowsPickingMultipleItems:YES];
    
    picker.prompt = NSLocalizedString(@"Add songs to play", @"Prompt in media item picker");
    
    [[super viewController] presentModalViewController:picker animated:YES];
}

- (void) mediaPicker: (MPMediaPickerController *) mediaPicker didPickMediaItems: (MPMediaItemCollection *) collection
{
    // @todo: くるくるを表示する
    
    // 音楽ファイルを取り出す
    // AVURLAssetを作成
    MPMediaItem *item = [collection.items lastObject];
    NSURL *url = [item valueForProperty:MPMediaItemPropertyAssetURL];
    AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url
                                               options:nil];
    
    // AVAssetExportSessionを生成
    AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:urlAsset
                                                                           presetName:AVAssetExportPresetAppleM4A];
    
    exportSession.outputFileType = [[exportSession supportedFileTypes] objectAtIndex:0];
    
    // artworkとmusicファイルを保存するためのURLを作成
    NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *baseFilePath = [docDir stringByAppendingPathComponent:[item valueForProperty:MPMediaItemPropertyTitle]];
    NSString *artworkPath = [baseFilePath stringByAppendingPathExtension:@"png"];
    NSString *musicFilePath = [baseFilePath stringByAppendingPathExtension:@"m4a"];
    
    // ArtworkのExport。
    UIImage *artwork = [[item valueForProperty:MPMediaItemPropertyArtwork] imageWithSize:CGSizeMake(100, 100)];
    NSData *artworkData = UIImagePNGRepresentation(artwork);
    
    bool artworkCreated = [artworkData writeToFile:artworkPath
                                        atomically:YES];
    
    // MusicファイルのExport。Exportは非同期に行われるので、完了時にJavascriptに値を返すようにする
    exportSession.outputURL = [NSURL fileURLWithPath:musicFilePath];
    // blocks用にselfを格納
    __block PGiTunesMusicLibrary *blockSelf = self;
    
    [exportSession exportAsynchronouslyWithCompletionHandler:^{
        NSMutableDictionary *dic;
        CDVPluginResult *result;   
        
        if (exportSession.status == AVAssetExportSessionStatusCompleted) {
            dic = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                   musicFilePath, @"musicUrl",
                   [item valueForProperty:MPMediaItemPropertyTitle], @"title", 
                   nil];
            
            // artworkがある場合のみ
            if (artworkCreated) {
                [dic setObject:artworkPath forKey:@"artworkUrl"];
            }
            
            // javascriptの成功時のコールバックに値を渡す
            result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dic];
            [blockSelf performSelectorOnMainThread:@selector(writeJavascript:)
                                        withObject:[result toSuccessCallbackString:blockSelf.callbackId]
                                     waitUntilDone:YES];
        } else {
            dic = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                   @"Music File Export Error.", @"message",
                   nil];
            
            // javascriptの失敗時のコールバックに値を渡す
            result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dic];
            [blockSelf performSelectorOnMainThread:@selector(writeJavascript:)
                                        withObject:[result toErrorCallbackString:blockSelf.callbackId]
                                     waitUntilDone:YES];
        }
        
        [[blockSelf viewController] performSelectorOnMainThread:@selector(dismissModalViewControllerAnimated:)
                                                     withObject:[NSNumber numberWithBool:YES]
                                                  waitUntilDone:YES];
    }];
    
}

- (void) mediaPickerDidCancel:(MPMediaPickerController *)mediaPicker
{
    [[super viewController] dismissModalViewControllerAnimated:YES];
}

@end


index.html


<!DOCTYPE HTML>

<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title></title>
        <script src="http://code.jquery.com/jquery-latest.js"></script>
        <script src="cordova-1.5.0.js" type="text/javascript"></script>
        <style type="text/css">
            #musicList {
                list-style: none;
                padding: 0;
                margin: 5px;
            }

            #musicList li .artwork {
                height: 50px;
                width: 50px;
                border: solid 1px #bbb;
                -webkit-box-shadow: 1px 1px 3px #000;
            }

            #musicList li div.artwork {
                background: -webkit-gradient(linear, left top, left bottom, from(#ddd), to(#fff));
                display: table-cell;
                vertical-align: middle;
                text-align: center;
                font-size: 0.8em;
            }
        </style>
    </head>
    <body>
        <audio id="player" controls></audio>
        <button id="btn">音楽を選択</button>
        <ul id="musicList"></ul>
        <script type="text/javascript">
            document.addEventListener('deviceready', function(){
                var iTunesMusicLibrary = {
                    showMusicPicker: function(types, success, fail) {
                        return Cordova.exec(success, fail, "jp.co.asial.iTunesMusicLibrary", "showMusicPicker", types);
                    }
                };

                var playMusic = function(musicUrl){
                    var $_player = $('#player');
                    $_player
                    .attr({
                        'src': musicUrl    
                    });
                    $_player.get(0).load();
                    $_player.get(0).play();
                };

                var addMusicItem = function(data){
                    var $_list = $('#musicList');

                    // artworkの有無を確認
                    var $_appendedImg;
                    if (data.hasOwnProperty('artworkUrl')) {
                        $_appendedImg = $('<img>')
                        .attr({
                            'src': data.artworkUrl,
                            'class': 'artwork'
                        })
                        .bind('touchend', function(){
                            playMusic(data.musicUrl);      
                        });
                    } else {
                        $_appendedImg = $('<div>')
                        .attr({
                            'class': 'artwork'
                        })
                        .text('No image')
                        .bind('touchend', function(){
                            playMusic(data.musicUrl);      
                        });
                    }

                    $_list.append(
                        $('<li>')
                        .append($_appendedImg)
                        .append(
                            $('<a>')
                            .attr({
                                'href': 'javascript:(function(){return undefined;}())',
                                })
                            .text(data.title)
                            .click(function(){playMusic(data.musicUrl)})
                        )
                    )
                }

                var MusicManager = function(){
                    if (!MusicManager.instance) {
                        var musicList = localStorage.getItem(MusicManager.key);
                        musicList = musicList ? eval('(' + musicList  + ')') : [];

                        this.setMusic = function(data, callback) {
                            data.id = new Date().getTime();
                            musicList.push(data);

                            localStorage.setItem(MusicManager.key, JSON.stringify(musicList));

                            // callbackがある場合は実行
                            if (callback  & & typeof callback === 'function') {
                                callback(data);
                            }
                        };

                        this.getMusic = function(musicId) {
                            if (musicId) {
                                var ret;
                                musicList.forEach(function(item, index, arr){
                                        if (!ret  & & item.id == musicId) {
                                        ret = item;
                                        }     
                                        });

                            } else {
                                ret = musicList;
                            }

                            return ret;
                        }

                        MusicManager.instance = this;
                    }

                    return MusicManager.instance;
                };

                MusicManager.getInstance = function(){
                    if (!this.instance) {
                        this.instance = new MusicManager();
                    }

                    return this.instance;
                }

                MusicManager.key = "MUSIC_LIST";

                $('#btn').click(function(){
                    iTunesMusicLibrary.showMusicPicker([], function(data){
                        // urlにファイルのURL、titleにタイトルが入っているので、localStorageに保存
                        MusicManager.getInstance().setMusic(data, addMusicItem);
                    }, function(error){
                        alert(error.message);
                    });
                });


                // 初期化処理
                MusicManager.getInstance().getMusic().forEach(function(item, index, arr){
                    addMusicItem(item);     
                });
            });
        </script>
    </body>
</html>


ではでは。