スマートデバイスアプリ開発のあれやこれや

MultipeerConnectivityで実現する近距離無線通信:序

 ふと思い立って,掲題のMultipeerConnectivity.frameworkを触ってみました。
近距離でのLINE風チャットアプリなんかが簡単に作れそうで楽しいAPIです……!
今回はMultipeerConnectivity.frameworkについて情報を整理してみました。

1. MultipeerConnectivityことはじめ

 MultipeerConnectivity.frameworkは,iOS7から使えるようになった近距離
無線通信を実現するためのフレームワークです。具体的には,Wi-FiとBlutoothを
使って,近くのiOSデバイスと通信するための基本機能をAPIで提供しています。
このフレームワークを使うと↓のようなアプリが作れます。

  • トランシーバーアプリ
  • チャットアプリ
  • 会議資料共有アプリ
  • ゲームの対戦機能

2. 機能のイメージ

 近距離通信するデバイスですが,大きく「ホスト」「クライアント」の2種類
存在しており,それぞれで微妙に役割が異なっています。「ホスト」は,1つの
通信グループ(同じサービス名を持つ通信)で1人だけ存在します。「ホスト」以外の
接続ユーザーは全て「クライアント」です。

f:id:cross-xross:20160122201308p:plain

 そして,クライアントは他のクライアントと直接通信できません。
クライアントが通信できるのは,ホスト1人だけ。そのため,接続ユーザー全員で
状況共有が必要な場合は,ホストが各クライアントに情報をブロードキャスト
する必要があります。


 また,1つの通信グループは最大でも8人までしか同時接続できないので
無尽蔵に大規模なグループは作れない……という点にも注意が必要です。


 MultipeerConnectivity.frameworkとは,言うなれば各接続ユーザに
「ホスト」「クライアント」の役割を与え,ユーザ間での通信を行うための
機能を提供するフレームワークといえます。

3. 各接続ユーザを識別する情報「Peer」

 次にPeerという概念について紹介します。Peerとは,接続ユーザーを
区別するための識別情報です。「ホスト」「クライアント」に関わらず
全てのユーザーがユニークなPeerを保持しています。

f:id:cross-xross:20160122204634p:plain

 MultipeerConnectivityの世界では,Peerを表現するためのクラスとして
MCPeerIDというクラスが用意されています。

4. 交信機能を提供する「Session」

 続いて,ユーザ間のメッセージングを実現するSessionについて説明します。
Peerと同じくSessionも各ユーザが1つ持っています。そして,自分が保持する
Sessionを使って他のユーザと交信(データ送信,データ受信)を行います。

f:id:cross-xross:20160122205812p:plain

 ちなみに,Sessionは内部属性として,前述のPeer,つまり誰がそのSession
の持ち主か?という情報と,サービス名という情報を持っています。サービス
名というのは,通信グループの識別名称と理解してください。Session同士が
交信を行うためには,サービス名が同じである,という前提条件があるので
注意してください。


f:id:cross-xross:20160122213448p:plain


 MultipeerConnectivityの世界では,Sessionを表現するためのクラスとして
MCSessionというクラスが用意されています。

5. 存在を主張する「Advertiser」

 ここまで,ユーザの識別情報を保持する「Peer」と,ユーザ間での交信を
担う「Session」について説明しました。では,実際のユーザ間の接続は
いったいどういう流れで行われるのでしょうか?そこには,「Advertiser」
という要素が絡みます。Advertiserとは,サービスの存在を主張する機能です。


 ユーザ間で交信をするにしても,最初はまずサービスへの接続が必要です。
そのためには,他のユーザに対してホストが,サービスの存在を主張する
必要があります。その存在主張をAdvertiserを通して行うのです。


f:id:cross-xross:20160122220550p:plain


MultipeerConnectivityの世界では,MCAdvertiserAssistantクラスが
Advertiserの機能を提供しています。

6. 次回予告

 今回は概念的な話で近距離通信を説明したので,次回は同じ話を実装レベル
で解説してみたいと思います。

今年個人的に注力したい技術

 あけましておめでとうございます。
今年も月に最低1件の技術ネタ投稿を目標にやっていきたいです。
さて,今年の抱負...ではないですが,今年は未経験の以下の技術について
調査しながら力を付けていきたいです。

 ・node.js
 ・Golang
 ・データベース一般知識

 去年初めて良かったことは継続し,反省すべきことは反省しで
今年も頑張っていきたいと思っています。
今後共よろしくお願いします。

Spring BootアプリをTomcatにデプロイする

 niwaka.hateblo.jp

 前回のSpringBoot記事から暫く空きました...。
気がつけばバージョンも1.3.0.Releaseまで到達しており
そのうち時間を取って知識を更新しなくてはと思っています。

 とはいえ、今回はまた別の話です。
SpringBootは組み込みTomcatの機能を備えているので
単体実行(APサーバ不要で)可能な.jarファイルが作成可能です。

 「こんなアプリ作ったんだけどどう?」みたいな感じで
誰かに簡単にアプリを試してもらうには便利な機能ですが
時たま「やっぱり.warファイル作りたい」というシーンがあります。

 今回はSpringBootアプリを.warファイルにまとめて
Tomcatにデプロイする方法を紹介します。

1. pom.xmlファイルを修正する

 .warファイル作成にあたって、追加で必要な
ライブラリがあるため、pom.xmlファイルを修正していきます。

  <modelVersion>4.0.0</modelVersion>
  <groupId>jp.co.cross_xross.web</groupId>
  <artifactId>SpringBootSample</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <!-- 変更1. パッケージを「war」に設定  -->
  <packaging>war</packaging>

  <!-- 中略 -->
    <!-- 変更2. jarの依存関係を追加する -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-tomcat</artifactId>
      <scope>provided</scope>
    </dependency>

2. SpringBootServletInitializerのサブクラスを作成する

 そのままでは、.warファイルを作成しても
web.xmlファイルが存在しないことから正常に起動できません。
そのため、Servlet起動時の設定情報を付与する必要があります。

 mainメソッドを持ったクラスに以下の変更を加えます。

  • SpringBootServletInitializerを継承する
  • configureメソッドをオーバーライドする

 

public class SampleController extends SpringBootServletInitializer {
// 中略

    public static void main(String args[]) {
	SpringApplication.run(SampleController.class, args);
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(SampleController.class);
    }

3. ビルドする

 あとは簡単。「mvn package」を実行すれば
targetディレクトリ配下に.warファイルが作成されます。
Tomcatのwebappディレクトリに.warファイルに置いて起動確認しましょう。


 ちなみにURLは「http://ホスト名/アプリ名/*」という形式になります。
単体実行可能な.jarファイルと形式が変わるので注意しましょう......。

4. トラブルシューティング的な話

 「.warファイルをデプロイしたんだけど、404エラーが...」
という人のための話を補足でちょっと書いておこうと思います。


 Tomcatが動いているJavaのバージョンと、Mavenビルド時に
設定したJavaのバージョンが異なると404エラーになることがあります。
(自分はMaven: 1.6。Tomcat:1.8だったのでハマりました......)


 Mavenで使うJavaのバージョン指定は以下の方法で出来ます。

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
  </properties>  


 Tomcatは、JAVA_HOMEの設定を使うので確認しましょう。

RxSwiftでUITableViewのバインド処理

niwaka.hateblo.jp

 ↑の記事で、UITableViewとのバインド時はセルが再利用されるので
Disposableをdispose関数で無効にする(キリッ)と言ってましたが

 もっと良い方法がありました......。

1. RxSwiftはUITableViewのバインド機能を提供している

 そうなんです。自前でちまちまバインド解除とか不要だったんです。
UITableViewやUICollectionView向けに簡単バインド機能を提供しているのです。

 どのくらい便利かというと......。

  • UITableViewDelegateの実装が不要
  • UITableViewDataSourceの実装が不要


 上記のプロトコルの実装=UITableViewの利用というイメージが
あったので、初めてこの機能を使ったときは半信半疑でした。
でも、RxSwiftを使うと動くのです......。

2. UITableViewとViewModelのバインドを定義する

 UITableViewに表示したいデータを保持するViewModelを定義します。

class ViewModel {
    /**
     仮面ライダーの名前を保持する配列
     */
    var riders = Variable<[String]>([])

    init() {
        riders.value.append("仮面ライダー龍騎")
        riders.value.append("仮面ライダーカブト")
        riders.value.append("仮面ライダーキバ")
        riders.value.append("仮面ライダーアギト")
        riders.value.append("仮面ライダーW")
        riders.value.append("仮面ライダーディケイド")
        riders.value.append("仮面ライダーオーズ")
        riders.value.append("仮面ライダー電王")
        riders.value.append("仮面ライダーウィザード")
    }
}


 そして、バインド対象のUITableViewを保持するController。

import UIKit
import RxSwift
import RxCocoa
import RxBlocking

class ViewController: UIViewController {

    /**
     TableView
     */
    @IBOutlet weak var list: UITableView!
    
    /**
     ViewModel
     */
    let model = ViewModel()
    
    /**
     Bag
     */
    let bag   = DisposeBag()
    
    /**
     Storyboardバインド後処理
     */
    override func viewDidLoad() {
        super.viewDidLoad()
        bindViewAndModel()
    }

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

    /**
     データバインド処理
     */
    private func bindViewAndModel() {
        // UITableViewとViewModelのバインド
        model.riders.bindTo(list.rx_itemsWithCellIdentifier("CellIdentifier")) { _, riderName, cell -> Void in
            cell.textLabel?.text = riderName
        }.addDisposableTo(bag)
    }

}

3. 実行結果

 見事バインドされております。

f:id:cross-xross:20151117212630p:plain

4. 補足情報

 UITableViewのセル押下時のイベントハンドリング用の関数や
セル削除の関数もRxSwiftが提供しているので基本的に前述のProtocolの
実装は不要......ですが、複雑なテーブル作成時にはRxSwiftではサポートが
難しい場合もあるような気がします。


 個人的には、SectionHeader対応とかが必要になってきたら
結局はUITableViewDataSourceの実装とかが必要なんだろうな、と。


 ただ、単純なテーブル作成であればすごく便利な機能だと思います。

RxSwiftでデータバインド -補足編-

 このエントリは、Qiitaに投稿した記事の補足です。

qiita.com

 上記の記事では、RxSwiftを使った双方向データバインドの
実現方法をまとめていますが、データバインドの解除方法についても
少し整理して書いておきたいと思います。

1. データバインドを解除しないと困るケース

 自分が遭遇した困るケースは、UITableViewとのデータバインド......。
UITableViewの中で扱われるUITableViewCellのインスタンスは使い回しされます。
例えば、7行表示のテーブルでは、Cellのインスタンスは7つだけ作成され
画面スクロール時も当該インスタンスのプロパティを更新する動きになります。


 そのため、CellとViewModelのインスタンス(Array)をバインドすると
ViewModelとControllerの1対1のバインド関係が崩壊してしまい
ViewModelを更新したタイミングで意図しないCellが書き換えられたりします。
1行目のCellとバインドされたViewModelを更新すると1行目のCellを流用した
○行目のCellがなぜか更新されてしまう......なんて感じですね。


 対策として、非表示になった行については
データバインドを解除してやることでViewModelとControllerのバインド
関係の整合性を保ちます。この方法を採ることで、データバインドされている
Cellは画面に表示されている部分だけ......という状況を作っています。

2. データバインド解除する方法

 簡単です。
データバインドの処理を書いたときのことを思い出します。

/**
 データバインド設定
 */
private func bindViewAndModel() {
    textField.rx_text.subscribeNext { [unowned self] text in
        self.viewModel.text = text
    }.addDisposableTo(bag)
}   


 上記コードのsubscribeNext関数はDisposable型インスタンスを返却します。
(返却されたインスタンスを即座にDisposableBagに突っ込んでますが......)


 このDisposable型インスタンスとは何か?という点がポイントになります。
Disposableは”バインドしたよ!”という保証書だと理解すると簡単です。
RxSwiftではDisposable(保証書)が有効である限りデータバインドを行います。
逆にいうとDisposableを失効させればバインドを解除できます。
Disposableクラスは、dispose関数を提供しているので実行しましょう。

/**
 データバインド設定
 */
private func bindViewAndModel() {
    let disposable = textField.rx_text.subscribeNext { [unowned self] text in
        self.viewModel.text = text
    }
    // データバインド解除
    disposable.dispose()
}   


 ね?簡単でしょう。

3. DisposeBagとは何か?

 Disposableが何か分かると、DisposeBagの役目も分かります。

class ViewController: UIViewController {

    @IBOutlet weak var textField: UITextField!

    let viewModel = ViewModel()
    let bag       = DisposeBag()
    // 中略
}


 Disposableインスタンスを保持するのがDisposeBagです。
その役目は即ち


 ”deinitのタイミングで保持している全Disposableを失効させる


ことです。
上記のコードで言うと
画面破棄のタイミングで自動的にデータバインドが解除されます。

 明示的にデータバインド解除を行わない限りは
DisposeBagを活用するのがベストプラクティスになると思います。
(解除を忘れると怖いですし......)

SwiftBondでデータバインドを実現する

 Modelを実行した後、画面更新処理をせこせこ実装するのが面倒...。
”更新処理を意識せずにiOSアプリが書けんものか”と思って色々調べてみると
SwiftBondというOSSライブラリを発見しました。
 github.com

 コイツを上手く使ってやると
ViewControllerとViewModel間でデータバインドが簡単に実現できます。
(ViewModelは必須ではないですが、Modelの実行結果の変換処理が
 ViewControllerから切り離せるのでViewModelがあった方が良いですね)

1. SwiftBondを導入する

 CocoaPodsを使って簡単インストールです。

use_frameworks!
pod 'Bond', '~> 4.0'

2. ViewModelを作成する

 ViewModelクラスを作成します。
今回はUITextFieldにバインドするString型変数1つを持ったクラスを作ります。

import Foundation
import Bond

class ViewModel {
    /**
     UITextFieldに対応する変数
     */
    var text = Observable<String?>("初期値")
}


 ポイントはString型で初期化するのではなく
SwiftBondが提供するObservableというWrapperクラスを使う所です。
(Observableクラスがデータバインドの機能を下支えしています......!)

3. ViewControllerを作成する

 ViewControllerクラスを作成します。
StoryboardとバインドされたUITextField型変数と先ほど作成した
ViewModel型変数の2つをメンバ変数として持たせておきます。

import UIKit
import Bond

class ViewController: UIViewController {

    /**
     バインド対象のUITextField
     */
    @IBOutlet weak var inputBox: UITextField!
    
    /**
     ViewModel
     */
    let viewModel = ViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }

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

4. ViewControllerとViewModelのBind処理を書き足す

 では、UITextFieldとViewModelをバインドしていきます。
今回はViewController側にバインド定義処理を実装したprivateな関数
「bindViewAndModel」を作りviewDidload関数から呼び出してみます。
(バインド定義が増えるとviewDidloadがゴミゴミするのでその対策)

    override func viewDidLoad() {
        super.viewDidLoad()
        bindViewAndModel()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    /**
     ViewとModelをバインドする
     */
    private func bindViewAndModel() {
        inputBox.bnd_text.bindTo(viewModel.text)
    }


 SwiftBondの提供する「bindTo」関数を使います。
これでUITextFieldの変更が自動でViewModelの変数textに伝搬します。
print関数でtextの中身を出力するような処理を書いて試してみてください。
ちなみに、「bindTo」関数は単方向のデータバインドを実現します。


 双方向のデータバインドを実現したい場合は
「bidirectionalBindTo」関数を使えばOKです。

    /**
     ViewとModelをバインドする
     */
    private func bindViewAndModel() {
        inputBox.bnd_text.bidirectionalBindTo(viewModel.text)
    }

補足. ViewModelの変数へのアクセス

 実際にバインドが成功しているか確認する場合の注意点です。
前述の通り、ViewModelの変数はObservable型として定義しているので
単純に print(viewModel.text) なんてしてもデータは正しく表示されません。
SwiftBondのvalue変数経由でUnwappingした値を取り出す必要があります。

    func pushCheckBtn() {
        print(viewModel.text.value!)
    }


 value変数はOptional型なのでさらにUnwrappすることも忘れずに!

Mockito事始め

 Javaのモックライブラリ Mockito(もきーと?)を使ってみたので
導入方法や簡単な使い方を整理してみます。

f:id:cross-xross:20150919155517p:plain

1. モックライブラリって何?

 モック=模造品、まがい物 という意味です。


 ある程度規模が大きいソフトウェアを開発するケースだと
自分担当のクラスは完成済みでテストしたいんだけど、クラスの中で
利用している別クラスが未完成
...なんてことはよくあると思います。
(未完成でもIFは設計済みなので完成できてしまう……!)


 よくある解決策として、未完成のソースコードをテスト用に書き換えて
とりあえずテストしてしまう……なんてやりがちですが、よくよく考えると
テストのためにテスト対象を書き換えることになり本末転倒です。


 そんなケースで、テストコードの中で未完成クラスを模造品(モック)に
置き換えてやる
ことで、テスト対象を汚すことなく完成済みクラスを
テストすることが出来ます。

2. Mockitoをインストールする

 公式サイトからMockitoのライブラリを入手しましょう。
Mavenをお使いの方であれば、pom.xmlに以下の記述を追加するだけでOKです。

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>1.10.19</version>
</dependency>

3. モックオブジェクトを使いたい状況の仮定

 では、早速Mockitoを使っていきます。
今回は完成済みクラスとして以下のような「SampleLogic」クラスが存在し

public class SampleLogic {
	
	private final String ODD  = "偶数";
	private final String EVEN = "奇数";
	// 未完成クラス
	private TargetClass target;
	
	/**
	 * Constructor
	 */
	public SampleLogic() {
		if (target == null) {
			target = new TargetClass();
		}
	}
	
	public String checkNumberType() {
		// 偶数と奇数を判断して文字列を返却する
		int randamNumber = target.generateRandamNumber();
		if (randamNumber % 2 == 0) {
			return ODD;
		} else {
			return EVEN;
		}
	}
}


 その内部で利用している「TargetClass」クラスが未完成という仮定で説明します。

public class TargetClass {
	
	public int generateRandamNumber() {
		//TODO: 乱数を返却する
		return 1;
	}
}


 TargetClassのgenerateRandamNumberメソッドは未完成で
ランダムな値を返すはずが固定値しか返却しないため、このままでは
命令網羅を実現できません。そのため、ランダムな数値を返却する
TargetClassのモック(模造品)を作る必要があります。

4. モック作成の準備

 テストクラス作成の準備をしていきます。
完成品を見た方が早いので晒してしまいます。

public class SampleLogicTest {
    @Mock
    private TargetClass mockObject;
    @InjectMocks
    private SampleLogic testTarget = new SampleLogic();
	
    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
    }
}


 今回のケースでは、未完成のTargetClassのモックを作成したいので
メンバ変数でTargetClassを定義した上で@Mockのアノテーションを付けます。
@Mock付きのメンバ変数がモック対象となる点を覚えておいてください。

    @Mock
    private TargetClass mockObject;

 

 一方、モックを利用する側のクラスについてもアノテーションが必要です。
SampleLogicが利用するので@InjectMockというアノテーションを設定します。
こちらはモックの挿入(インジェクト)先を指定する意味を持ちます。

    @InjectMocks
    private SampleLogic testTarget = new SampleLogic();


 そして、忘れちゃいけない設定があります。
今回はアノテーションベースでMockitoを扱うため@Before付きの
前処理メソッドで初期化処理を実装しておく必要があります。

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
    }

5. モック作成

 実際にモックを作成していきます。
基本的には@Mock付きのmockObjectを弄るだけです。
(インジェクト設定は既に済んでますので)

@Test
public void test() {
	// generateRandamNumberメソッドのモックを作成する
	when(mockObject.generateRandamNumber()).thenReturn(2);
	String result = testTarget.checkNumberType();
	assertEquals("偶数", result);
}


 whenメソッドでモックしたいメソッド(generateRandamNumber)を指定します。
そしてそのまま、thenReturnメソッドの引数で戻り値で返したい値を設定します。
基本的にwhen(どのメソッドをモックする?)/then(どのようにモックする?)という感じで直感的です。


 結果、checkNumberTypeメソッドの内部で実行されるTargetClassは
模造品(モック)で置き換えられているので、generateRandamNumberメソッドも2を返します。
それによって、checkNumberTypeメソッドも偶数を返すようになりテストは成功する......。

6. モックしずらいテストクラス

 とはいえ、Mockitoも万能ではありません。
例えば今まで説明に使っていたテスト対象クラスがこう変わったらどうでしょうか?

	public String checkNumberType() {
		TargetClass target  = new TargetClass();
		int randamNumber = target.generateRandamNumber();
		if (randamNumber % 2 == 0) {
			return ODD;
		} else {
			return EVEN;
		}
	}


 @InjectMockアノテーションモックを差し込めるのは
メンバ変数だけなので、ローカル変数になってしまうとお手上げです。

そのため、設計段階からMockitoでテストすることを意識することが重要です。


 とはいえ、メンバ変数に切り出せない(例:スレッドセーフにならないクラス)場合も
ありえるのでそういう場合は泣く泣く諦めるしかありません......。
Mockitoは便利だけれど、万能ではない、という点を覚えておくと良いと思います。