J2ObjCでRxJavaをiOSライブラリ化する


# はじめに

「[J2ObjCでJavaとSwiftを連携させる](http://qiita.com/k5n/items/270eb3ee4c62abfa6408)」で紹介したようにJ2ObjCを利用することで、iOS/Androidでモデル部分をJavaで共通化することができます。

最近はスマホアプリの開発でC#発祥のMVVM的なView-ViewModel-Modelに分割するパターンが有用と認識されてきています。MVVMでは各層のやりとりはコマンドとイベントで行うため、イベント監視[^1]にObserverパターンを用いることになるかと思います。

[^1]: 特に１対多になるViewModel-Model間やModel同士の

Observerパターンをさらに拡張したやはりC#発祥のReactive Extension(略してRx)はMVVMと相性が良く、最近関連記事を多く見かけます。

というわけでRxのJava版、RxJavaをJ2ObjCで利用できるか試してみました。何度もJava -> Objective-C変換やコンパイルするのは時間の無駄なので、スタティックライブラリ化します。利用したのはRxJava 1.1.0です。

# RxJavaのコード修正

J2ObjCは１ファイル複数クラスをうまく扱えないようです。RxJavaには rx/internal/util/unsafe/BaseLinkedQueue.java の中に複数クラスが含まれています。これをまず１ファイル１クラスになるように複数ファイルに分割しておきます。

# Xcodeプロジェクト作成

XcodeからiOSのCocoa Touch Static Libraryプロジェクトを作成してください。名前はRxJavaとしておきましょう。

RxJavaフォルダの中身は実ファイルごと削除しておきます。

# JavaからObjective-Cへの変換

Javaソースはもういじらないので、Objective-Cへの変換はあらかじめやっておきます。シェルスクリプトにしました。スクリプトにしておけばJ2ObjCがバージョンアップした時に楽に再変換かけられます。

RxJavaのソースはsrcに格納しています。

```bash:translate.sh
#!/bin/sh

DERIVED_FILES_DIR=RxJava
SOURCE_PATH=src
J2OBJC_DIST=~/Library/j2objc
JAVA_SOURCES=`find ${SOURCE_PATH} -type f -name *.java`

rm ${DERIVED_FILES_DIR}/*

for INPUT_FILE_PATH in ${JAVA_SOURCES};
do
    ${J2OBJC_DIST}/j2objc -d ${DERIVED_FILES_DIR} \
        -sourcepath ${SOURCE_PATH} \
        --no-package-directories ${INPUT_FILE_PATH};
done
```

実行すると、変換されたファイルがRxJavaフォルダに出力されます。

```shell-session
$ ./translate.sh
```

# Xcodeプロジェクトの設定

Xcode上のRxJavaグループに、生成されたファイルを全て追加します。

Build Settingsで

 - USER_HEADER_SEARCH_PATHSに"${HOME}/Library/j2objc/include"を追加
 - CLANG_ENABLE_OBJC_ARCをNOにしてARCを無効に

# universalライブラリのビルド

実機とシミュレータの両方のバイナリを含んだuniversalバイナリを作成するシェルスクリプトを作成しました。

```bash:build.sh
#!/bin/sh

#-----------------
# Settings
#-----------------

TARGET=RxJava
LIB_FILE_NAME=lib${TARGET}.a
OUTPUT_DIR=Release/${TARGET}
CONFIGURATION=Release
#CONFIGURATION=Debug

#-----------------
# Clean up
#-----------------
xcodebuild -configuration ${CONFIGURATION} -target ${TARGET} clean
rm -rf build
rm -rf ${OUTPUT_DIR}

#-----------------
# Build
#-----------------
xcodebuild -configuration ${CONFIGURATION} -target ${TARGET} -sdk iphonesimulator
[ $? -ne 0 ] && exit 1
xcodebuild -configuration ${CONFIGURATION} -target ${TARGET} -sdk iphoneos
[ $? -ne 0 ] && exit 1

#-----------------
# Copy library
#-----------------
mkdir -p ${OUTPUT_DIR}/include

lipo -create \
build/${CONFIGURATION}-iphoneos/${LIB_FILE_NAME} \
build/${CONFIGURATION}-iphonesimulator/${LIB_FILE_NAME} \
-o ${OUTPUT_DIR}/${LIB_FILE_NAME}

#-----------------
# Copy headers
#-----------------
cp ${TARGET}/*.h ${OUTPUT_DIR}/include
for f in `ls ${TARGET}/*.h`; do
    echo '#import "'${f##*/}'"';
done > ${OUTPUT_DIR}/include/RxJava.h
```

実行するとReleaseの中に結果が格納されます。

```shell-session
$ ./build.sh
```

# 利用設定

まずは[以前の記事](http://qiita.com/k5n/items/270eb3ee4c62abfa6408)の通り、Javaで記述するModel部分(MVVMならViewModeとModel)をstatic libraryターゲット化してください。

先ほどReleaseの中にできたRxJavaフォルダをまるごとプロジェクトのルートにコピーします。プロジェクトにRxJavaフォルダごと追加します。

アプリターゲットにリンクの設定がされているか確認してください。されていなければ追加してください。Model側は逆にリンクしていないことを確認してください。

![link_rxjava.png](https://qiita-image-store.s3.amazonaws.com/0/53805/a0926b63-4366-3317-d85b-be6b27d6871e.png)

LIBRARY_SEARCH_PATHSに"$(PROJECT_DIR)/RxJava"を追加されているか確認してください。されていなければ追加してください。

Modelターゲットとアプリターゲットの両方のUSER_HEADER_SEARCH_PATHSに"$(PROJECT_DIR)/RxJava/include"を追加します。

RxJavaのjarファイルがないとJavaファイルの時点でRxJavaが見えません。RxJavaフォルダのなかにrxjava-1.1.0.jarファイルを入れておきます。

![directory_hierarchy_with_rxjava.png](https://qiita-image-store.s3.amazonaws.com/0/53805/9534a0e0-b9ae-2684-edd6-feca302f8ef0.png)

ModelターゲットのBuild Rules - Java source files using Scriptのスクリプト部分に"-classpath ${PROJECT_DIR}/RxJava/*.jar"引数を追加します。

変更後のスクリプトは以下のようになります。

```bash
${HOME}/Library/j2objc/j2objc -d ${DERIVED_FILES_DIR} \
    -classpath ${PROJECT_DIR}/RxJava/*.jar \
    --prefixes ${PROJECT_DIR}/Model/prefixes.properties \
    --no-package-directories ${INPUT_FILE_PATH};
```

アプリ側でRxJavaのクラスが見えるように、RxJava.hをModel.hかSampleJ2ObjC-Bridging-Header.hで#importしておきます。

```objc:Model/Model.h
#import "Config.h"
#import "RxJava.h"
```

# 利用してみる

## Model側

```java:Model/app/model/Config.java
package app.model;

import rx.Observable;
import rx.subjects.BehaviorSubject;

class Config {
    private static final int DEFAULT_SPEED = 20;

    private BehaviorSubject<Integer> _speed;

    Config() {
      reset();
    }

    public void reset() {
        _speed.onNext(DEFAULT_SPEED);
    }

    public Observable<Integer> getSpeed() {
        return _speed;
    }

    public void setSpeed(int speed) {
        _speed.onNext(speed);
    }
}
```

## アプリ側

Model側の中でだけ使うなら普通にJavaで使うのと同じです。問題はこれをアプリ側のSwiftから使う場合です。subscribeWithRxFunctionsAction1()メソッドを呼び出すのですが、これがRxFunctionsAction1プロトコルを実装したオブジェクトを要求します。

```swift:ViewController.swift
import UIKit

class ViewController: UIViewController {
  private let config = Config()
  private var speedSubscription: RxSubscription?

  override func viewWillAppear(animated: Bool) {
    // Swiftに無名クラスってないの(ToT)
    // しょうがないからRxFunctionAction1プロトコルを満たすクラスを作ってオーバーライド
    class SpeedAction : NSObject, RxFunctionsAction1 {
      @objc func callWithId(t: AnyObject!) {
        if let value = t as? Int {
          print("speed changed: " + String(value))
        }
      }
    }
    let speed = config.getSpeed()
    speedSubscription = speed.subscribeWithRxFunctionsAction1(SpeedAction())
  }

  override func viewWillDisappear(animated: Bool) {
    speedSubscription?.unsubscribe()
  }
}
```

Swiftではラムダが使えるから無名クラスって仕組みはないっぽい。Javaの無名クラス作って渡す仕組みは使いづらいですね。

## ラッパー

SwiftからRxJavaを使うならラッパーを作った方が良さそうです。こんな感じ？

```swift:RxJavaUtil.swift
import Foundation

/// RxSubscriptionを格納しておき、消滅時にまとめてunsubscribeする
class SubscriptionBag {
  private var _subscriptions = [RxSubscription]()

  func add(subscription: RxSubscription) {
    _subscriptions.append(subscription)
  }

  deinit {
    _subscriptions.forEach { $0.unsubscribe() }
  }
}

/// SubscriptionBagに格納する拡張メソッドを提供
extension RxSubscription {
  func addToBag(subscriptionBag: SubscriptionBag) {
    subscriptionBag.add(self)
  }
}

/// 関数をRxFunctionAction1でラップするクラス
class Action1Binder<T>: NSObject, RxFunctionsAction1 {
  private let _action: T -> Void

  init(action: T -> Void) {
    _action = action
  }

  func callWithId(t: AnyObject!) {
    if let value = t as? T {
      _action(value)
    }
  }
}

/// RxObserbableへの購読で引数に関数を使えるようにする拡張
extension RxObservable {
  func subscribeOnNext<T>(onNext: T -> Void) -> RxSubscription {
    return subscribeWithRxFunctionsAction1(Action1Binder(action: onNext))
  }
}
```

利用する側は

```swift:ViewController.swift
import UIKit

class ViewController: UIViewController {
  private let config = Config()
  private var subscriptionBag = SubscriptionBag()

  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.

    let action: Int -> Void = { print("speed changed: " + String($0)) }
    config.getSpeed().subscribeOnNext(action).addToBag(subscriptionBag)
  }
}
```

subscribeOnNext&lt;Int&gt; { value in /* ... */ } って書くとエラーになります。少なくともSwift 2.1ではジェネリクス関数の型を明示するのはダメみたいです。この辺りはもうちょっと書きやすいように工夫したいですね。

## 変換後のクラス名やメソッド名

変換後のクラス名はrx.functions.Action1はRxFunctionsAction1になって長ったらしいです。名前が衝突しないなら全てRx[^2]になるようにした方が扱いやすそうです。方法については[以前の記事](http://qiita.com/k5n/items/270eb3ee4c62abfa6408)に書いたのでそちらを参照してください。

[^2]: あるいはObjective-Cの慣習に従ってRXと大文字２文字にするか

メソッド名の変換もカスタマイズできるみたいなので、気にいらなければ改造できます。[J2ObjCのChanging Method Namesのページ](http://j2objc.org/docs/Changing-Method-Names.html)を参照してください。