2015年9月15日火曜日

SwiftでCordova(ionic)のpluginを開発するには?

Swiftでcordovaのプラグインを開発することはできるのだろうか?

このあたりを見ると開発できそうだ。

Chris Dell - Software Developer :: Writing an iOS Cordova plugin in pure Swift

How to write Cordova plugin in Swift? - Stack Overflow

やってみよう。

今回はChrisさんのコードを参考に、HTMLからネイティブに小文字のメッセージを送って大文字変換されたメッセージを受信するというサンプルを作る。これが完成形。


ビルドシステムはionicを使うが、プラグイン部分の開発はcordovaでも通用するはず。

まずはblankプロジェクトを作ろう。

$ ionic start CDVEchoPlugin blank

CDVEchoPlugin/www/index.htmlをこのように編集する。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
    <title>CDVEchoPlugin</title>

    <link href="lib/ionic/css/ionic.css" rel="stylesheet">
    <link href="css/style.css" rel="stylesheet">

    <!-- IF using Sass (run gulp sass first), then uncomment below and remove the CSS includes above
<link href="css/ionic.app.css" rel="stylesheet">
-->

    <!-- ionic/angularjs js -->
    <script src="lib/ionic/js/ionic.bundle.js"></script>

    <!-- cordova script (this will be a 404 during development) -->
    <script src="cordova.js"></script>

    <!-- your app's js -->
    <script src="js/app.js"></script>
  </head>
  <body ng-app="starter" ng-controller="controller">

    <ion-pane>
      <ion-header-bar class="bar-stable">
        <h1 class="title">CDVEchoPlugin</h1>
      </ion-header-bar>

      <ion-content padding="true">
        <label class="item item-input">
          <span class="input-label">msg.send</span>
          <input type="text" ng-model="msg.send">
        </label>
        <label class="item item-input">
          <span class="input-label">msg.return</span>
          <input type="text" ng-model="msg.return">
        </label>
        <button class="button button-block button-positive" ng-click="onClickBtnEcho();">echo</button>
      </ion-content>

    </ion-pane>
  </body>
</html>

CDVEchoPlugin /www/js/app.jsをこのように編集する。

/* jshint -W117, -W035, -W072, -W003 */
'use strict';


// Ionic Starter App

// angular.module is a global place for creating, registering and retrieving Angular modules
// 'starter' is the name of this angular module example (also set in a <body> attribute in index.html)
// the 2nd parameter is an array of 'requires'
angular.module('starter', ['ionic'])

  .run(function($ionicPlatform) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      StatusBar.styleDefault();
    }
  });
}).controller('controller', function($scope) {

  console.log('controller init');

  $scope.msg = {
    'send': 'message to send',
    'return': ''
  };

  $scope.onClickBtnEcho = function() {
    console.log('onClickBtnEcho: msg.send=' + $scope.msg.send);
    if (window.EchoPlugin) {
      EchoPlugin.echo($scope.msg.send, function(returnMsg) {
        $scope.msg.return = returnMsg;
        console.log('onClickBtnEcho: msg.return=' + $scope.msg.return);
        $scope.$applyAsync();
      });
    } else {
      console.log('NO EchoPlugin');
    }
  };

});

ここまでで一応起動しておこう。

$ cd /path/to/CDVEchoPlugin/
$ ionic run ios


カスタムプラグインのディレクトリを作る。

$ mkdir plugins-dev
$ cd plugins-dev
$ mkdir EchoPlugin

次の内容でCDVEchoPlugin/plugins-dev/EchoPlugin/plugin.xmlを作る。

<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
        id="echo-plugin"
        version="0.1">

  <name>EchoPlugin</name>
  <description>This plugin just echoes uppercased string.</description>

  <js-module src="echo-plugin.js">
    <clobbers target="window.EchoPlugin" />
  </js-module>

  <!-- iOS -->
  <platform name="ios">
    <config-file target="config.xml" parent="/*">
      <feature name="EchoPlugin">
        <param name="ios-package" value="EchoPlugin" />
      </feature>
    </config-file>
    <source-file src="src/ios/EchoPlugin.swift" />
  </platform>

</plugin>

さらに次の内容でCDVEchoPlugin/plugins-dev/EchoPlugin/echo-plugin.jsを作る。

'use strict';

var exec = require('cordova/exec');

var EchoPlugin = {

  echo: function(sendMsg, onSuccess, onFail) {
    return exec(onSuccess, onFail, 'EchoPlugin', 'echo', [sendMsg]);
  }

};

module.exports = EchoPlugin;

Swiftのソースディレクトリを作る。

$ mkdir src
$ cd src
$ mkdir ios
$ cd ios

次の内容でCDVEchoPlugin/plugins-dev/EchoPlugin/src/ios/EchoPlugin.swiftを作る。

import Foundation

@objc(EchoPlugin) class EchoPlugin : CDVPlugin {

    func echo(command: CDVInvokedUrlCommand) {

        var message = command.arguments[0] as! String
        message = message.uppercaseString // Prove the plugin is actually doing something

        var pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAsString: message)
        commandDelegate.sendPluginResult(pluginResult, callbackId:command.callbackId)

    }

}

プロジェクトのトップディレクトリに移動し、作ったプラグイン「EchoPlugin」をプロジェクトにインストールする。

$ cd /path/to/CDVEchoPlugin
$ ionic plugin add plugins-dev/EchoPlugin/

ここで次の項目を確認する。


  • plugins-dev/EchoPluginの内容がplugin/EchoPluginにコピーされているか?
  • CDVEchoPlugin/platforms/ios/CDVEchoPlugin/config.xmlにplugin.xmlで設定したfeatureの内容が追加されているか?
  • CDVEchoPlugin/platforms/ios/CDVEchoPlugin/Plugins/echo-plugin/EchoPlugin.swiftがあるか?
  • CDVEchoPlugin/platforms/ios/www/plugins/echo-plugin/echo-plugin.jsがあるか?


SwiftはiOS8以降なので最低OSを8にする必要がある。そこで、CDVEchoPlugin/config.xmlに次のように「<preference name="deployment-target" value="8.0" />」の記述を加える。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<widget id="com.ionicframework.cdvechoplugin107512" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
・・・・・・
  <preference name="deployment-target" value="8.0" />
</widget>

そして、この設定をCDVEchoPlugin/platforms/ios/CDVEchoPlugin.xcodeprojに反映させる。

$ ionic prepare

次の内容でCDVEchoPlugin/platforms/ios/CDVEchoPlugin/Classes/Bridging-Header.hを作る。

//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//

#import <Cordova/CDV.h>

これはSwiftからObjective-Cのライブラリ(Cordovaなど)にアクセスするために必要。でなければコンパイルエラーになる。ただ作っただけではダメで、Xcodeにて手動で設定する必要がある。

XcodeでCDVEchoPlugin/platforms/ios/CDVEchoPlugin.xcodeprojを開く。

先ほどターゲットOSを指定するためにprepareした効果は、General→Deployment Info→Deployment Targetで確認できる。



「Build Settings」タブに移り、「All」をクリックして、「Objective-C Bridging」で検索フィルターをかけ、「Objective-C Bridging Header」に「CDVEchoPlugin/Classes/Bridging-Header.h」を設定する。

追記15.09.16: ※ここでObjective-C Bridging Headerが見つからない場合は、Xcode上でClassesに適当にswiftファイルを追加すると「自動的に設定しますか」的なことを言われる場合がある。OKすると勝手にClasses/プロジェクト名-Bridging-Header.hを作ってくれる。



ここまでで、とりあえずProduct→Buildでコンパイルできるはず。

しかし、Bridging Headerにはさらに落とし穴があって、この状態で実行するとこのようなエラーが出る。

dyld: Library not loaded: @rpath/libswiftCore.dylib
Referenced from: /private/var/mobile/Containers/Bundle/Application/2E6CCCD1-E46C-43F8-8F1F-1204A1991455/CDVEchoPlugin.app/CDVEchoPlugin
Reason: image not found

なぜ出るのか?解決方法は?

iphone - dyld: Library not loaded: @rpath/libswiftCore.dylib - Stack Overflow

一番usefulがついている"Embedded Content Contains Swift Code"の方法では解決しなかったが、"Runpath Search Paths"の方法で解決した。

「Build Settings」タブに移り、「All」をクリックして、「runpath」で検索フィルターをかけ、「Runpath Search Paths」に「@executable_path/Frameworks」を設定する。


ここまでやればProduct→Runで実行できるはず。


「echo」ボタンをクリックすると「msg.return」に大文字になってエコーが返ってくる。単純ではあるが、Swiftで開発したプラグインが正常に動作している証拠だ。

これでSwiftでプラグインを書ける!何でもデキる気がしてきた∠( ゚ω゚)/