Kotlin で iOS アプリを作る

概要

Kotlin で iOS アプリを作成します。 開発環境には Android Studio を使用し、Intel Multi-OS Engine を使って iOS アプリに変換して実行します。

確認環境

参考情報

解説

前提

Xcode, Android Studio, Kotlin Plugin はインストール済みとする。

Multi-OS Engine のインストール

Android Studio に Multi-OS Engine (MOE) Plugin をインストールする。

プロジェクトの作成

プロジェクトの作成手順は通常の Android プロジェクトと同じ。

Multi-OS Engine モジュールの作成

iOS アプリは Multi-OS Engine モジュールとして作成する。

モジュールを作成したら、実行可能な状態になっているので Run 'ios' を選択して実行する。 実機での実行は Developer Team ID が必須な様子(How to configure the Development Team id with MOE 1.2.0 - Support - Multi-OS Engine)。 Simulator では実行された。

Kotlin に変換

Multi-OS Engine モジュールのソースコードJava で書かれている。 Java を Kotlin に変換する。

まずは、build.gradle に Kotlin の設定を加える。 以下を行うとプロジェクトと app (Android アプリ) モジュールに Kotlin の設定が追加される。

ios (Multi-OS Engine モジュール) の build.gradle にはエディタで以下を追記する。

apply plugin: 'kotlin'

dependencies {
    ...
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

次に、Javaソースコードを Kotlin に変換する。

app (Android アプリ) モジュールは java フォルダを選択し Code > Convert Java File to Kotlin File を選択すれば変換される。

ios (Multi-OS Engine モジュール) でも java フォルダを選択し Code > Convert Java File to Kotlin File を選択すれば変換されるが、一部編集が必要。

変換前の Java ファイルと変換後の Kotlin ファイルを記載する。

Main.java

@RegisterOnStartup
public class Main extends NSObject implements UIApplicationDelegate {

    public static void main(String[] args) {
        UIKit.UIApplicationMain(0, null, null, Main.class.getName());
    }

    @Selector("alloc")
    public static native Main alloc();

    protected Main(Pointer peer) {
        super(peer);
    }

    private UIWindow window;

    @Override
    public boolean applicationDidFinishLaunchingWithOptions(UIApplication application, NSDictionary launchOptions) {
        return true;
    }

    @Override
    public void setWindow(UIWindow value) {
        window = value;
    }

    @Override
    public UIWindow window() {
        return window;
    }
}

Main.kt

@RegisterOnStartup
class Main protected constructor(peer: Pointer) : NSObject(peer), UIApplicationDelegate {

    companion object {

        @JvmStatic fun main(args: Array<String>) {
            UIKit.UIApplicationMain(0, null, null, Main::class.java.name)
        }

        @Selector("alloc")
        @JvmStatic external fun alloc(): Main
    }

    private var window: UIWindow? = null

    override fun applicationDidFinishLaunchingWithOptions(application: UIApplication?, launchOptions: NSDictionary<*, *>?): Boolean {
        return true
    }

    override fun setWindow(value: UIWindow?) {
        window = value
    }

    override fun window(): UIWindow? {
        return window
    }
}

alloc 関数では override を削除し @JvmStatic external を追加する。 window 関数の戻り値の型が UIWindow になっているため UIWindow? に変更する。

AppViewController.java

@org.moe.natj.general.ann.Runtime(ObjCRuntime.class)
@ObjCClassName("AppViewController")
@RegisterOnStartup
public class AppViewController extends UIViewController {

    @Owned
    @Selector("alloc")
    public static native AppViewController alloc();

    @Selector("init")
    public native AppViewController init();

    protected AppViewController(Pointer peer) {
        super(peer);
    }

    public UILabel statusText = null;
    public UIButton helloButton = null;

    @Override
    public void viewDidLoad() {
        statusText = getLabel();
        helloButton = getHelloButton();
    }

    @Selector("statusText")
    @Property
    public native UILabel getLabel();

    @Selector("helloButton")
    @Property
    public native UIButton getHelloButton();

    @Selector("BtnPressedCancel_helloButton:")
    public void BtnPressedCancel_button(NSObject sender){
        statusText.setText("Hello Intel Multi-OS Engine!");
    }
}

AppViewController.kt

@org.moe.natj.general.ann.Runtime(ObjCRuntime::class)
@ObjCClassName("AppViewController")
@RegisterOnStartup
class AppViewController protected constructor(peer: Pointer) : UIViewController(peer) {

    @Selector("init")
    external override fun init(): AppViewController

    val statusText: UILabel
        get() = getStatusTextSel()

    val helloButton: UIButton
        get() = getHelloButtonSel()

    override fun viewDidLoad() {}

    @Selector("statusText")
    @Property
    external fun getStatusTextSel(): UILabel

    @Selector("helloButton")
    @Property
    external fun getHelloButtonSel(): UIButton

    @Selector("BtnPressedCancel_helloButton:")
    fun BtnPressedCancel_button(sender: NSObject) {
        statusText.setText("Hello Intel Multi-OS Engine!")
    }

    companion object {

        @Owned
        @Selector("alloc")
        @JvmStatic external fun alloc(): AppViewController
    }
}

alloc 関数は override を削除して @JvmStatic external とする。 実装の無い Selector は external を付ける。 関数名を getStatusText とするとプロパティの statusText と衝突してしまうため getStatusTextSel としている。 自動変換では getLabel 関数が label プロパティに変換されてしまうので修正する。 statusText, helloButton プロパティは var から val に変更し、get をオーバーライドしている。 Java と同様に viewDidLoad 関数が呼ばれたタイミングでフィールドにオブジェクトを保持したい場合は lateinit var を使えば良い。

以上で変換は完了となるため、再度実行して動作することを確認する。

共通モジュールの作成

app (Android モジュール) と ios (Multi-OS Engine モジュール) で共有するモジュール (Java モジュール) を作成する。 app と同様に Kotlin に変換できる。

app と ios モジュールの依存関係に common モジュールを追加する。

以上により共通モジュールのクラスを app と ios モジュールから使用できる。

今後の予定

  • iOS アプリの UI 編集方法の検証
  • 通化できる範囲の検証

Kotlin の書籍