Apple Watchで霧吹きを作る方法(前編)

apple-watch-moyashidx-eyecatch

 

本記事でできること

・Apple WatchとiPhoneの通信
ソースコピペで実装

 

 

1.開発環境

OS X El Capitan 10.11.2
Xcode7.2.1
Watch OS 2.1
Cocos2dx 3.2(既存プロジェクトが3.2なので)

 

 

2.既存プロジェクトにWatchKit Appを追加

2.1 WatchKit Appを追加するプロジェクト

apple-watch-moyashidx2-2-1

世界ともやし業者界隈で話題沸騰中の衝撃のもやし育成ゲーム「もやしびと」の完全新作である「もやしびとDX」にWatchKit Appを追加しました。

ちなみにDXというのは

_人人人人人人人人人人人人人_
> デリシャス エクスタシー <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

のことです!

 

2.2 ターゲットを追加する

左下の+アイコンをクリック。

apple-watch-moyashidx2-2-2

 

watchOSのApplicationにWatchKit Appがあるので選択してNextボタンクリック後設定を行います。

apple-watch-moyashidx2-2-3

 

正常に追加できると下図のようになります。

apple-watch-moyashidx2-2-4

 

 

3.Apple WatchとiPhone間で通信

3章での完成形は、WatchとiPhoneアプリそれぞれに送信ボタンを設置し、データを受け取った時にデータを示すところまでとなります。

 

3.1 Apple Watchに受信処理を実装

Apple Watchでメッセージを受信したら受信したデータを示すようにします。Watchアプリを開発するための道具は2章の操作によって既に追加されているので、それらを使ってWatchとiPhone間で霧吹きに必要なデータ(ダミー)のやりとりを実装します。Watch OS2.0よりWatch Connectivityが使えるようになったということなので、WatchConnectivity.frameworkを追加します。

apple-watch-moyashidx2-3-1

 

私はSwiftを選択したので基本的にはInterfaceController.swiftにコードを書き込んでいきます。ひとまず、通信周りは別クラスとしてWCSessionLogic.swiftに書き込むことにしました。またデリゲートを使ってUIの更新をしてもらいます。

WCSessionLogic.swift

import WatchKit
import Foundation
import WatchConnectivity

class WCSessionLogic: NSObject, WCSessionDelegate {

  private var m_interfaceControllerDelegate:InterfaceControllerDelegate?
  var interfaceControllerDelegate: InterfaceControllerDelegate {
      get {
          return self.m_interfaceControllerDelegate!
      }
      set (newValue) {
          m_interfaceControllerDelegate = newValue
      }
  }

  /// 初期化
  func initSession() {
    let session = WCSession.defaultSession()
    session.delegate = self 
    session.activateSession()
  }

  /// UserInfoTransfer 
  func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
    dispatch_async(dispatch_get_main_queue(), { () in
      self.parceMessage(userInfo)
    })
  }

  /// Application Context
  func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {
    dispatch_async(dispatch_get_main_queue(), { () in
      self.parceMessage(applicationContext) })
  }

  /// Interactive Messaging
  func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
    parceMessage(message)
    replyHandler(["Reply" : "Success"])
  }

  /// iPhoneから受信したデータをパースする
  func parceMessage(message: [String : AnyObject]) {
    dispatch_async(dispatch_get_main_queue(), { () in
      if let replyMessage:[String: AnyObject]? = message as [String: AnyObject]? {
        for (key, val) in replyMessage! {
          if let keyStr:String? = String(key) {
            switch keyStr! {
              case "SprayRecoveryTime":
                if let valueStr:String? = String(val) {
                  if let floatValue:Float? = Float(valueStr!) {
                    self.m_interfaceControllerDelegate?.updateUI(String(floatValue!))
                  }
                }
              default:
                break;
            }
          }
        }
      }
    })
  }
}

 

InterfaceController.swift

import WatchKit
import Foundation

protocol InterfaceControllerDelegate
{
    /// 受信データをラベルに反映する
    func updateUI(receivedMessage:String)
}

class InterfaceController: WKInterfaceController, InterfaceControllerDelegate {

    /// iPhoneと通信するクラス
    private var m_wcsessionLogic: WCSessionLogic!
    
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
    }

    override func willActivate() {
        super.willActivate()
        initLogic()
    }

    override func didDeactivate() {
        super.didDeactivate()
    }

    /// ロジック初期化
    func initLogic() {
        m_wcsessionLogic = WCSessionLogic()
        m_wcsessionLogic.initSession()
        m_wcsessionLogic.interfaceControllerDelegate = self
    }
}

 

updateUIを実装するために、受信したデータを表示するためのLabelを用意します。現在のところ、UIを調整する手段としてはInterface.storyboardを使う以外ないとのこと。objective-cの時からxibも使いたくない派だったので、storyboardはあまり使いたくないですが仕方ありません。右下の項目からLabelをドラッグアンドドロップで配置します。

apple-watch-moyashidx2-3-2

 

配置したLabelをコードに紐付ける必要があるので、Labelの上でcontrolを押しながらドラッグしてコードにドロップします。

apple-watch-moyashidx2-3-3

 

これでupdateUIを下記のように実装できます。

InterfaceController.swift

import WatchKit
import Foundation

protocol InterfaceControllerDelegate
{
    /// 受信データをラベルに反映する
    func updateUI(receivedMessage:String)
}

class InterfaceController: WKInterfaceController, InterfaceControllerDelegate {
    
    /// 受信したメッセージ
    @IBOutlet var m_receivedText: WKInterfaceLabel!

    /// iPhoneと通信するクラス
    private var m_wcsessionLogic: WCSessionLogic!
    
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
    }

    override func willActivate() {
        super.willActivate()
        initLogic()
    }

    override func didDeactivate() {
        super.didDeactivate()
    }

    /// ロジック初期化
    func initLogic() {
        m_wcsessionLogic = WCSessionLogic()
        m_wcsessionLogic.initSession()
        m_wcsessionLogic.interfaceControllerDelegate = self
    }
    
    /// UI更新
    func updateUI(receivedMessage:String) {
        m_receivedText.setText("受信データ:" + receivedMessage)
    }
}

 

3.2 Apple Watchに送信処理を実装

Watchアプリで送信ボタンを押したらiPhoneアプリにメッセージを送信します。ButtonLabelと同様に配置して下さい。Buttonの場合は押した時の処理を追加しなければならないので、Button上で右クリックしてSent Actionsをドラッグしてコード上にドロップします。ここではsendMessageという関数を追加しました。

apple-watch-moyashidx2-3-4

 

WCSessionLogicクラスに送信用の関数を追加します。Watchアプリから送信ボタンが押される度に霧吹き情報を送信するため、送信方法はUserInfoTransferにしました。必要に応じて通信方式を変更して下さい。

WCSessionLogic.swift

    @IBAction func sendMessage() {
        dispatch_async(dispatch_get_main_queue(), { () in
            self.m_wcsessionLogic.sendMessage(["spray":"霧吹きしてね"])
        })
    }
}

 

3.3 iPhoneに送信・受信処理を実装

iPhoneでメッセージを受信した事を示すのはログ出力にします。RootViewControllerに下記コードを追加すると送信処理と受信処理が利用できるようになります。

RootViewController.h

#import < UIKit/UIKit.h >

@interface RootViewController : UIViewController {

}
- (BOOL) prefersStatusBarHidden;

/** iPhoneとWatch間データの同期 */
- (void)sendContextMessage:(NSDictionary*)sendData;

@end

 

RootViewController.mm

#import "RootViewController.h"
#import "cocos2d.h"
#import "platform/ios/CCEAGLView-ios.h"
#import < WatchConnectivity/WatchConnectivity.h >

@implementation RootViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]))
    {
        if ([WCSession isSupported])
        {
            WCSession *session = [WCSession defaultSession];
            session.delegate = self;
            [session activateSession];
        }
    }
    return self;
}

-略-

#pragma mark - WCSession

/**
 * Application Contextでデータを送信
 * @param sendData 送信データ
 */
- (void)sendContextMessage:(NSDictionary*)sendData
{
    NSError *error = nil;
    [[WCSession defaultSession] updateApplicationContext:sendData error:&error];
}

#pragma mark - WCSessionDelegate

/**
 * Apple WatchからBackGround通信メッセージを受信する
 */
- (void)session:(WCSession *)session didReceiveUserInfo:(NSDictionary< NSString *,id > *)userInfo
{
    dispatch_async(dispatch_get_main_queue(), ^{
        
        if ([userInfo objectForKey:@"spray"] != nil)
        {
            NSLog(@"受信データ:%@", [userInfo objectForKey:@"spray"]);
        }
        
    });
}

 

iPhoneアプリから送信ボタンを押した時にデータを送信するためにWCSessionNativeBridgeクラスを追加します。

WCSessionNativeBridge.h

#ifndef WCSessionNativeBridge_h
#define WCSessionNativeBridge_h

#include "cocos2d.h"

namespace Cocos2dExt {
    
    class WCSessionNativeBridge
    {
    public:
        /**
         * Application Contextでデータを送信
         * @param sendData 霧吹き情報
         */
        static void sendContextMessage(std::map< std::string, std::string > sendData);
    };
    
} // end of namespace Cocos2dExt

#endif /* WCSessionNativeBridge_h */

 

WCSessionNativeBridge.mm

#include "WCSessionNativeBridge.h"
#import "AppController.h"

namespace Cocos2dExt
{
    void WCSessionNativeBridge::sendContextMessage(std::map< std::string, std::string > sendData)
    {
        NSMutableDictionary* sendDic = [NSMutableDictionary dictionary];
        for (auto it = sendData.begin(); it != sendData.end(); ++it)
        {
            NSString* keyStr = [NSString stringWithUTF8String:(*it).first.c_str()];
            NSString* valueStr = [NSString stringWithUTF8String:(*it).second.c_str()];
            [sendDic setObject:valueStr forKey:keyStr];
        }
        
        AppController *appController = (AppController *)[UIApplication sharedApplication].delegate;
        [appController.viewController sendContextMessage:sendDic];
    }
}

 

警告が消えなかったのでAppController.hを修正

AppController.h

#import < UIKit/UIKit.h >
#include "RootViewController.h"

@interface AppController : NSObject < UIApplicationDelegate > {
    UIWindow *window;
}

@property(nonatomic, readonly) RootViewController* viewController;

@end

 

HelloWorld.cppに実装されているボタンの処理内容を修正します。

HelloWorld.cpp

void HelloWorld::menuCloseCallback(Ref* pSender)
{
    std::map< std::string, std::string > sendData;
    sendData["SprayRecoveryTime"] = "10.0";
    Cocos2dExt::WCSessionNativeBridge::sendContextMessage(sendData);
}

 

 

4.実機で起動する

4.1 バージョンとビルド番号を揃える

ここでビルドすると下図のエラーが出ます。これはiPhoneとWatchのターゲットのバージョン、ビルド番号が異なることが原因です。全て一致するように修正します。

apple-watch-moyashidx2-4-1 apple-watch-moyashidx2-4-2 apple-watch-moyashidx2-4-3

 

4.2 プロビジョニングを作る

ここが面倒なところですが、各ターゲットのプロビジョニングを作らなければなりません。下記3つに対応したプロビジョニングを作りました。リリース用にも必要なのでさらに増えます。。。

・jp.co.technoroid.watchroid-dev
・jp.co.technoroid.watchroid-dev.watchkitapp
・jp.co.technoroid.watchroid-dev.watchkitapp.watchkitextension

 

4.3 実行する

うまく通信できてます (・ω<) 

apple-watch-moyashidx2-4-4

 

apple-watch-moyashidx2-4-5

 

 

5.次回の内容

・Apple WatchでHealthKit
・Apple WatchでのUI調整の辛さ