SwiftからPepperの音量を変更してみた

#はじめに
Pepperの音量を手軽に変更できるiOSアプリとかないのかな？という声が聞かれたので、勉強がてら作ってみよう！と、いうことで作成するに至ったPepper向けの音量変更アプリについてのまとめです。

今回、SwiftからPepperに動作させる命令を投げるにあたって、 [NAOqiのSDKs](http://doc.aldebaran.com/2-5/dev/programming_index.html)を見る限り、直接のやりとりはできないということで、[QiMessagingでiOSとPepperを相互にやりとりするサンプル](http://satoshin2071.github.io/qimessagingdeiostopepperwoxiang-hu-niyaritorisurusanpuru/)を参考にして、WebViewJavascriptBridgeを使用してJavascriptからAPIを呼ぶ手法を取らせていただきました。
※XcodeのATSの設定を無効にする必要があると書かれているのですが、私が開発に使用した環境では無効にしなくても動作したため、今回ATSの設定は行なっておりません。
※参考にさせていただいた先のコードは、Objective-C、qimessaging1.0、WebViewJavascriptBridgeのversion5を使用していますので、参考にする場合はその辺りに注意してください。

#開発環境
- Xcode 8
- Swift 3.1

#ライブラリのインストール
CocoaPodsを使用して[WebViewJavascriptBridge](https://github.com/marcuswestin/WebViewJavascriptBridge)（ver6）をインストールします。以下のように`pod 'WebViewJavascriptBridge', '~> 6.0'`をPodfileに追加してpod installしてください。

```:Podfile
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'WVJBSwift' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for WVJBSwift
  pod 'WebViewJavascriptBridge', '~> 6.0'

end
```
CocoaPodのインストール等についてわからない場合は、こちら[【Swift】CocoaPods導入手順](http://qiita.com/ShinokiRyosei/items/3090290cb72434852460)の記事等を参考にして導入してみてください。
※WebViewJavascriptBridgeはSwiftとJavaScriptの間でメッセージの送信を行うためのライブラリです。

#JavaScript側の処理について
使用しているHTMLのコード全文は後述しています。参照してみてください。
###1.ヘッダ
qimessagingのJavaScriptファイルを読み込みます。今回はqimessaging2を使用するので、以下のように記述してください。

```html
<script src="libs/qi/2/qi.js"></script>      
```
使用するjsファイルについては、[Aldebaranのgithubアカウント](https://github.com/aldebaran/libqi-js)で公開されておりますので、libs以下を取得してプロジェクトに追加してください。

###2.WebViewJavascriptBridge
WebViewJavascriptBridge用の関数を追加します。必ず必要なもののため忘れずに追加してください。

```js
function setupWebViewJavascriptBridge(callback) {
    if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
    if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
    window.WVJBCallbacks = [callback];
    var WVJBIframe = document.createElement('iframe');
    WVJBIframe.style.display = 'none';
    WVJBIframe.src = 'https://__bridge_loaded__';
    document.documentElement.appendChild(WVJBIframe);
    setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
```

Swiftとメッセージのやり取りをするための処理は以下の記述内に記載します。

```js
setupWebViewJavascriptBridge(function(bridge){
    // ここに記述
}
```

Swift側から呼び出す処理（ハンドラ）の記述は以下のようにしてください。

```js
bridge.registerHandler("ハンドラ名", function(data, responseCallback) {
    // 処理内容
}
```
ハンドラ名は任意です。Swift側で呼び出す際にここで設定した名前を使用してください。
※今回はregisterHandlerしか使用しませんが、機会があればcallHandlerについても記載したいと思います。

###3.qimessaging
今回はPepperの操作をするためにJavaScriptからNAOqi APIを使用して動作の命令を呼び出します。

**3.1.初期化**

```js:初期化
QiSession(function(session){
    //connected
},function (){
    //disconnected
}
, data)
```
`function(session){}`内はセッションが確立できた際に実行されます。sessionにQiSessionオブジェクトが渡されます。
`function(){}`内はセッションが切断された際に実行されます。
`data`には接続先のホスト名もしくはIPアドレスを設定します。

**3.2.サービスの取得**
セッションを確立できたら、サービスの取得を行います。

```js:サービスの取得
[取得したセッション].service("サービス名").then(function(service){
    //OK
}, function(error){
    //NG
});
```
サービス名は操作したい内容に合わせて設定してください。（参考：[NAOqi API](http://doc.aldebaran.com/2-5/naoqi/index.html)）
`function(service){}`内はサービスが取得できた際に実行されます。serviceにサービスへのプロキシが渡されます。
`function(){}`内はエラーが発生した際に実行されます。

**3.3.サービスの使用**
サービスを使用してPepperを操作します。下記は音量を変更する際の例です。APIの仕様についてはNAOqi APIのドキュメントを参考にしてください。

```js:サービスの使用
[プロキシ].setOutputVolume([音量]).then(function(){
    //OK
}, function(error){
    //NG
});
```

#Swift側の処理について
今回使用しているコードの全文は後述していますので、参照してみてください。
###１.Main.storyboard
今回はMain.storboardについての詳細説明は省略しますが、画面上にUIWebView、UISlider、Button（２つ）を追加します。（ボタンの名称、オブジェクトのサイズは任意）
《画面例》
![2017-08-02 18.05.27.png](https://qiita-image-store.s3.amazonaws.com/0/195038/fc42e163-644c-7ab0-7e76-2ca18f75cf52.png)

※Sliderについて
デフォルトで、`Continuous Updates`がオンになっていますが、これをオンにしているとスライダーを動かした際に常にイベント（ValueChanged）が発生することになります。
![2017-08-03 15.07.13.png](https://qiita-image-store.s3.amazonaws.com/0/195038/9736b3c4-c8b8-69cb-5fe3-3be96a2fd6ed.png)
今回はValueChanged時にPepperに音量変更の命令を送りたいため、これをオンにしているとスライダー操作時に常にPepperとの通信が発生することになるため、注意してください。（その程度のトラフィックは問題ないぜ！という場合はオンのままでどうぞ）
オフにすると、スライダーから手を離したタイミングでイベントが発生します。

###2.ViewController.swift
以下のようにWebViewJavascriptBridgeをインポートしてください。

```swift:import
import UIKit
import WebViewJavascriptBridge
```

UIWebViewを使用するので、UIWebViewDelegateプロトコルを追加してください。

```swift:
class ViewController: UIViewController, UIWebViewDelegate {
```

**2.1.アプリ起動時の処理**
アプリ起動時に、先ほど作成したHTMLページの読み込みとWebViewJavascriptBridgeのインスタンスの生成を行います。

```swift:viewDidLoad
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //WebViewへHtmlベージの読み込み
        self.loadHtmlPage(webView: self.webView)
        WebViewJavascriptBridge.enableLogging()
        //Bridgeの生成
        self.createBridge()
    }
```

```swift:loadHtmlPage
    func loadHtmlPage(webView: UIWebView){
         //読み込むHTMLファイルの設定
        let htmlPath = Bundle.main.path(forResource: "WVJBQi2", ofType: "html")
        do{
            let appHtml = try String(contentsOfFile: htmlPath!, encoding: String.Encoding.utf8)
            let baseURL = URL.init(fileURLWithPath: htmlPath!)
            //URLの情報をWebViewに読み込む
            webView.loadHTMLString(appHtml, baseURL: baseURL)
        }catch{
            print("Html読み込みエラー")
        }
    }
```
`Bundle.main.path(forResource: "ファイル名", ofType: "拡張子")`で読み込むHTMLファイルへのパスを取得します。
`try String(contentsOfFile: "ファイルへのパス", encoding: [エンコード形式])`でHTMLファイルの内容を取得します。
`URL.init(fileURLWithPath: "ファイルへのパス")`で読み込むURLのパスを生成します。
`webView.loadHTMLString([HTMLファイルの内容], baseURL: [URLのパス])`をWebViewに設定します。

```swift:createBridge
    func createBridge(){
        self.bridge = WebViewJavascriptBridge.init(forWebView: mywebView)
    }
```
`WebViewJavascriptBridge.init(forWebView: [UIWebView])`でHTMLを表示するUIWebViewのインスタンスを渡してWebViewJavascriptBridgeのインスタンスを生成します。

**2.2.Pepperへ命令を投げる処理**
JavaScriptの処理を使用するためにはSwift側でcallHandlerを使用します。

```swift:callHandler
self.bridge.callHandler("ハンドラ名", data: [JSへ渡す値]) {(responseData) in
    //レスポンスが返ってきた時の処理
    }
```
ハンドラ名はJSで設定したものを合わせるようにしてください。
dataに設定する値はJS側で使用したいものを設定してください。nilでも構いません。

**例）**
以下は接続処理のコードです。接続処理時はレスポンスで返ってきた値が"success"の場合に接続成功時の処理を行っています。

```swift:connect
    @IBAction func connect(_ sender: Any) {
        //HTMLからstartConnectをCallする
        self.bridge.callHandler("startConnect", data: pepperIp) {(responseData) in
            let res = responseData as! String
            if(res == "success"){
                //接続
                print("Connected")
                let alert = UIAlertController(title: "接続完了", message: "接続しました", preferredStyle: .alert)
                //アラートを閉じた時の動作を設定
                alert.addAction(UIAlertAction(title: "OK", style: .default, handler: {
                    (action:UIAlertAction!) -> Void in
                    self.getNowVolume()
                }))
                self.present(alert, animated: true, completion: nil)

            }else{
                //切断
                print("Disconnected")
            }
        }
    }
```
#サンプルソース
今回ここに載せてあるコードは、実際に作成したアプリのものとは異なりますが、必要な動作をするかは確認済みです。
Swif側の説明のMain.storyboardの部分も参考にした上で、試しに動かしてみてください。

###WVJBQ2.html
今回のHTMLの全文は以下になります。

```html:WVJBQi2.html
<!doctype html>
<html><head>
    <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
        <style type='text/css'>
            html { font-family:Helvetica; color:#222; }
            h1 { color:steelblue; font-size:24px; margin-top:24px; }
            button { margin:0 3px 10px; font-size:12px; }
            .logLine { border-bottom:1px solid #ccc; padding:4px 2px; font-family:courier; font-size:11px; }
        </style>
        <script src="libs/qi/2/qi.js"></script>      
</head>
<body> 
    <h1>WebViewJavascriptBridge Demo NAOqi2</h1>
    <script>
        window.onerror = function(err) {
            log('window.onerror: ' + err)
        }
    
    // WebViewJavascriptBridgeのREADMEに記述されてた準備処理
    function setupWebViewJavascriptBridge(callback) {
        if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
        if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
        window.WVJBCallbacks = [callback];
        var WVJBIframe = document.createElement('iframe');
        WVJBIframe.style.display = 'none';
        WVJBIframe.src = 'https://__bridge_loaded__';
        document.documentElement.appendChild(WVJBIframe);
        setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
    }
    
    // bridgeさせたい処理はこの中に記述
    setupWebViewJavascriptBridge(function(bridge)
                                 {
                                 //log出力
                                 var uniqueId = 1
                                 function log(message, data) {
                                 var log = document.getElementById('log')
                                 var el = document.createElement('div')
                                 el.className = 'logLine'
                                 el.innerHTML = uniqueId++ + '. ' + message + ':<br/>' + JSON.stringify(data)
                                 if (log.children.length) { log.insertBefore(el, log.children[0]) }
                                 else { log.appendChild(el) }
                                 }
                                 
                                 var qiSession;
                                 var self = this;
                                 
                                 /** Swiftから叩かれるメソッドを登録 **/
                                 // pepperと通信を開始する
                                 // 引数：data(IPアドレス)
                                 bridge.registerHandler('startConnect', function(data, responseCallback) {
                                                        // 接続先IPアドレス（data）
                                                        log('Call Connect to Pepper With IPAddress ', data)
                                                        //接続(NAOqi ver.2)
                                                        QiSession(function(session){
                                                                  qiSession = session;
                                                                  //サービスの取得
                                                                  qiSession.service("ALTextToSpeech").then(function(tts){tts.say("接続しました。");})
                                                                  log('Service OK')
                                                                  //使用するサービスの初期設定
                                                                  init();
                                                                  var responseData = { 'Javascript Says':'Right back atcha!' }
                                                                  log('connect Success !!', responseData)
                                                                  responseCallback("success")
                                                                  },function (){
                                                                  log('disconnected')
                                                                  responseCallback("disconnected")
                                                                  }
                                                                  , data)
                                                        })
                                 
                                 // 話すコマンドを送る
                                 bridge.registerHandler('say', function(data, responseCallback) {
                                                        if(self.animatedSpeech){
                                                            log('say ',data)
                                                        
                                                        self.animatedSpeech.say(data).then(function(){
                                                            log('say OK')
                                                            responseCallback("success :" + data)
                                                        }, function(error){
                                                            log('error ', error)
                                                            responseCallback(error)
                                                        });
                                                        }else{
                                                        disconnected();
                                                        }
                                })

                                 // 音量を取得する
                                 bridge.registerHandler('getVolume', function(data, responseCallback) {
                                                        log('getVolume ',data)
                                                        if(self.audioDevice){
                                                            self.audioDevice.getOutputVolume().then(function(volume){
                                                                log('Voume ',volume)
                                                                responseCallback(volume)
                                                            }, function(error){
                                                                log('error ', error)
                                                                responseCallback(error)
                                                            });
                                                        }else{
                                                        disconnected();
                                                        }
                                 })

                                 // 音量を変更する
                                 // 引数：data(音量[0-100])
                                 bridge.registerHandler('changeVolume', function(data, responseCallback) {
                                                        log('setVolume ',data)
                                                        if(self.audioDevice){
                                                        self.audioDevice.setOutputVolume(data).then(function(){
                                                                                                log('setVolume OK')
                                                                                                responseCallback("Volume Change success")
                                                                                                }, function(error){
                                                                                                log('error ', error)
                                                                                                responseCallback(error)
                                                                                                });
                                                        }else{
                                                        disconnected();
                                                        }
                                                        })
                                 
                                 /** init  **/
                                 //ALProxyのインスタンスを保持する
                                 function init(){
                                 
                                 log('初期化処理')
                                 //会話系 self.textToSpeech
                                 qiSession.service("ALAnimatedSpeech").then(function(as){
                                                                          self.animatedSpeech = as;
                                                                          }, function(error){
                                                                          log('An error occurred ',error);
                                                                          });
                                 //オーディオ入出力系 self.audioDevice
                                 qiSession.service("ALAudioDevice").then(function(ad){
                                                                          self.audioDevice = ad;
                                                                          }, function(error){
                                                                          log('An error occurred ',error);
                                                                          });
                                 };
                                 document.body.appendChild(document.createElement('br'))

                                 })
        </script>
    <div id='log'></div>
</body></html>

```


###ViewController.swift
今回のViewController.swiftの全文は以下になります。
IBActionの設定は、UIButtonが`Touch Up Inside`でUISliderが`Value Changed`です。

```swift:ViewController.swift
import UIKit
import WebViewJavascriptBridge

class ViewController: UIViewController {
    @IBOutlet weak var mywebView: UIWebView!
    @IBOutlet weak var myConnectButton: UIButton!
    @IBOutlet weak var myTalkButton: UIButton!
    @IBOutlet weak var myVolSlider: UISlider!
    var bridge :WebViewJavascriptBridge!            //WVJB接続用
    let pepperIp = "xxx.xxx.xxx.xxx"                //PepperのIPアドレス（適宜変更する）
    
    override func viewDidLoad() {
        super.viewDidLoad()

        //WebViewへHtmlベージの読み込み
        self.loadHtmlPage(webView: mywebView)
        WebViewJavascriptBridge.enableLogging()
        //Bridgeの生成
        self.createBridge()

    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func connect(_ sender: Any) {
        //HTMLからstartConnectをCallする
        self.bridge.callHandler("startConnect", data: pepperIp) {(responseData) in
            let res = responseData as! String
            if(res == "success"){
                //接続
                print("Connected")
                let alert = UIAlertController(title: "接続完了", message: "接続しました", preferredStyle: .alert)
                //アラートを閉じた時の動作を設定
                alert.addAction(UIAlertAction(title: "OK", style: .default, handler: {
                    (action:UIAlertAction!) -> Void in
                    self.getNowVolume()
                }))
                self.present(alert, animated: true, completion: nil)
            }else{
                //切断
                print("Disconnected")
            }
        }
    }
    
    @IBAction func tapTalkButton(_ sender: Any) {
        //話す内容の設定
        let talkText = "こんにちは"

        //HTMLからsayをCallする
        bridge.callHandler("say", data: talkText){(responseData) in
            if(responseData != nil && (responseData as! String) != ""){
                print("say Call Success: \(responseData as! String)")
            }else{
                print("say Call Error")
            }
        }
    }
    
    @IBAction func sliderValueChanged(_ sender: Any) {
        //音量の変更処理
        //スライダーの値を取得する
        let volume = Int(myVolSlider.value)
        
        //HTMLからchangeVolumeをCallする
        self.bridge.callHandler("changeVolume", data: volume){(responseData) in
            if(responseData != nil && (responseData as! String) != ""){
                //音量変更が成功した場合は変更後の音量がコールバックで返ってくる
                print("音量変更のためのjsを叩いたコールバック：\(responseData as! String)")
            }else{
                print("changeVolume Call Error")
            }
        }
    }
    
    //現在の音量の取得
    func getNowVolume(){
        //HTMLからgetVolumeをCallする
        self.bridge.callHandler("getVolume", data: nil) {(responseData) in
            if (responseData != nil) {
                //コールバックに数値が返ってきたら取得した値でスライダーの初期値を変更する
                let volume = responseData as! Int
                self.myVolSlider.setValue(Float(volume), animated: true)
            }else{
                print("getVolume Call Error")
            }
        }
    }

    //HtmlページをWebViewに読み込む
    func loadHtmlPage(webView: UIWebView){
         //読み込むHTMLファイルの設定
        let htmlPath = Bundle.main.path(forResource: "WVJBQi2", ofType: "html")
        do{
            let appHtml = try String(contentsOfFile: htmlPath!, encoding: String.Encoding.utf8)
            let baseURL = URL.init(fileURLWithPath: htmlPath!)
            //URLの情報をWebViewに読み込む
            webView.loadHTMLString(appHtml, baseURL: baseURL)
        }catch{
            print("Html読み込みエラー")
        }
    }
   
    //JSとSwiftを接続するためのブリッジを生成する
    func createBridge(){
        self.bridge = WebViewJavascriptBridge.init(forWebView: mywebView)
    }
}
```

#おわりに
今回はObjective-CをSwiftにしたり、qimessaging1.0をversion2にしたり、できたと思ったら動かなくて必死でデバッグしたりと大変なこともありましたし、反省点もいろいろとありますが、勉強になったと思っています。
この中で記載している内容で、何かございましたらコメントしていただけると幸いです。


#参考にさせていただいたサイト
- [WebViewJavascriptBridge 6.0.2](https://github.com/marcuswestin/WebViewJavascriptBridge)
- [ NAOqi 2.5.5](https://github.com/aldebaran/libqi-js)
- [QiMessagingでiOSとPepperを相互にやりとりするサンプル](http://satoshin2071.github.io/qimessagingdeiostopepperwoxiang-hu-niyaritorisurusanpuru/)