Kotlin/Nativeを使ってXcode + CLionでiOSアプリを書く

# はじめに
この記事はエムスリーAdvent Calendarの19日目の記事です。昨日は@maeharinによる[サーバーサイドKotlin](https://qiita.com/maeharin/items/78325dbd629e4e6d2fdb)についての話でした。

私はAndroidをメインとしたアプリエンジニアなので、本日はなぜか[Kotlin Advent Calendar](https://qiita.com/advent-calendar/2017/kotlin)でも18日現在触れられていないiOSアプリをKotlinで書く話をします。
（Kotlinアドベントカレンダーの方はうかうかしてたら埋まってました😥）

またアプリと記事を書いてから気付いたのですが、RettyさんのAdvent Calendarで既にKotlinでiOSアプリを書かれている先駆者がいらっしゃいました。
https://qiita.com/noripi/items/4ee969c48b3da5ca6fbd

そのため現状まともにKotlin/Nativeを記述できる唯一のIDEであるCLionを用いる部分に少し重きを置いて解説いたします。

# Kotlin/Nativeについて
以前どこでもKotlinという弊社主催のKotlin勉強会でv0.3時代のKotlin/Nativeの解説を行いましたので、概要はそちらをご覧ください。

[Starting Kotlin/Native おなじところ、ちがうところ #m3kt](https://speakerdeck.com/oboenikui/native-onazitokoro-tigautokoro-number-m3kt)

ただし、バージョンアップにより上の発表時点との差異が出てきています。大きな変更点を簡単に挙げると、

- Gradleファイルの書き方が変わった
- CMakeでもビルド可能になった
- **割とまともにiOSアプリを書けるようになった**

という感じです。3番目は[KotlinConfのアプリ](https://github.com/JetBrains/kotlinconf-app)がKotlinで書かれていることでも話題になりましたね。
そこで本記事ではiOSアプリをKotlinで書いてみます。

# 注意事項
以下の情報については一般的に解説されている部分については説明いたしません。これらについて経験のない方は、一般的な情報を調べていただければと思います。

- XcodeでStoryboardを用いてiOSアプリを作る方法
- CMakeについて
- Gradleについて

# 環境構築
Macに以下のソフトウェアをインストールします。

- Xcode 9
- CLion 2017.3.1 EAP
    - Kotlinプラグイン
    - Kotlin/Nativeプラグイン
- CocoaPods

CLionはJetBrainsが作っているC/C++向けのIDEです。All Products PackかCLion単品をご購入ください。CLion単品なら個人ライセンスで1万弱/年で購入可能です。1日あたり27円ちょいです。
ちなみに私は昨年まで学生だったので、ディスカウントを利用してAll Products Packを購入しました。
経済を回していきましょう。
https://www.jetbrains.com/store/
http://samuraism.com/products/jetbrains

学生であればIndividual Licenseを無料で使用できるのでそちらを使うと良いと思います。
https://www.jetbrains.com/student/

動作環境は、現在のところ64ビット版iOSが動くデバイスのみとなっています。シミュレータやiPhone 5以前のiPhone, 4世代目以前のiPadなどでは動かないので注意が必要です。筆者は第4世代のiPadしか持っていなかったので、この記事を執筆する機会にiPadを買い替えました。
経済を回していきましょう。
ちなみにこの記事の数日前にシミュレータ対応のプルリクエストがマージされていましたので次期バージョンではシミュレータで動かせると思います。

# ワークスペースのディレクトリ構成
現状複数のソフトウェアやビルドツールを跨いでビルドする必要があるため、ディレクトリ構成は若干複雑になります。
基本的にはXcodeプロジェクトにKotlin/Native用のファイルが追加された形になります。
KotlinConfアプリの構成を参考に載せておきます。

```
root
  ├── KotlinCMakeModule
  │    └── (Kotlin/Native用CMakeファイル, 主にCLion用)
  ├── gradle
  |    └── (Gradle Wrapperファイル群)
  ├── src
  │    └── main
  │       ├── kotlin
  │       │    └── (Kotlinのソース・ファイル)
  │       └── c_interop
  │            └── (cinteropの定義ファイル)
  ├── (プロジェクト名)
  │    └── 通常のXcodeプロジェクトと同様
  ├── (プロジェクト名).xcodeproj
  ├── (プロジェクト名).xcworkspace
  ├── gradlew
  ├── build.gradle
  └── CMakeLists.txt (CLion用)
```

# プロジェクトの作成
## Xcodeでの作業
今回は単一のプロジェクトしか使わないので、ワークスペースは作成しません。

1. Xcodeで新しいプロジェクトを作成します。
2. プロジェクトの種類はSingle View App、言語はObjective-Cを選択します。
3. プロジェクトの設定でBuild Phasesに以下のような`Run Script Phase`を2つ追加します。適宜名前をつけることをお薦めします。  
`./gradlew -p $SRCROOT compileKonanApp`  
`cp "$SRCROOT/build/konan/bin/iphone/app.kexe" "$TARGET_BUILD_DIR/$EXECUTABLE_PATH"`
4. 自動的に生成されたObjCなどのファイルはそのままにしておきます。

## CLionでの作業
以下の作業は、Kotlin/Native関連のCMake関連ファイルをコピーするために行う作業です。既に他のプロジェクトがある場合は、`KotlinCMakeModule`ディレクトリと、以下の`CMakeLists.txt`をルートにコピーするだけで問題ないです。

### CLionの一時的なプロジェクトを作成
Xcodeのプロジェクトを作成したディレクトリとは別の、空のディレクトリに新規プロジェクトを作成してください。
Kotlin/Nativeプラグインを入れていれば、Kotlin/Native Applicationを選択できるはずなので、そちらを選択します。
現在はサンプルからしか選べず、またiOSのサンプルは選べないので、"Hello World"プロジェクトを選択します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/15121/c5d392a1-c0a6-82e8-927d-937a5f23d5ba.png)

### `KotlinCMakeModule`の抽出
CLionを終了し、作ったディレクトリから`KotlinCMakeModule`ディレクトリをXcodeのプロジェクトを作成したディレクトリにコピーします。CLionで作ったディレクトリはもう不要なので削除してください。

### ソースディレクトリの作成
`src/main/kotlin`ディレクトリを作成します。

### CLionを再度開く
CLionをXcodeのプロジェクトのディレクトリで開きます。今はまだ`CMakeLists.txt`がないので警告が出るはずです。

#### `CMakeLists.txt`の作成
以下のような`CMakeLists.txt`を作成し、プロジェクトのルートに配置します。

```cmake:CMakeLists.txt
cmake_minimum_required(VERSION 3.8)

set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/KotlinCMakeModule)

project("iOS Sample" Kotlin)

konanc_executable(
        NAME ios-kotlin-native-sample
        TARGET iphone
        SOURCES src/main/kotlin/
)
```

キャッシュを削除してリロードします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/15121/e05f4771-41fe-51aa-9717-cf0c88938d36.png)

これで`src/main/kotlin`ディレクトリ内にKotlinファイルを配置するとIDE支援を受けることができます。
※ ただしめちゃくちゃ重い
![clion_suggestion.gif](https://qiita-image-store.s3.amazonaws.com/0/15121/670c197a-4514-905e-db3a-5741cb73757a.gif)

## Gradleの配置、記述
一般的なGradleプロジェクトと同様にGradle Wrapper (v3.3) を配置します。

また、以下のように`build.gradle`を記述します。

```groovy:build.gradle
buildscript {
    repositories {
        mavenCentral()
        maven {
            url "https://dl.bintray.com/jetbrains/kotlin-native-dependencies"
        }
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-native-gradle-plugin:0.3.4"
    }
}

apply plugin: "konan"

konan.targets = ["iphone"]

konanArtifacts {
    program("app")
}
```

# アプリの記述
基本的には、Kotlinファイルの編集はCLion、StoryboardやObjCなどXcode由来のファイルはXcodeで記述します。
また、GradleはIntelliJ IDEAで編集すると良いかもしれません。

## 標準出力Hello World
まずは`main.kt`ファイルを作成します。Kotlinは必ずしもクラスごとにファイルを分ける必要がなく、今回は記述量が少ないため、全てを`main.kt`に書くことにします。

```kotlin:main.kt
import kotlinx.cinterop.*
import platform.Foundation.*
import platform.UIKit.*

fun main(args: Array<String>) {
    memScoped {
        val argc = args.size + 1
        val argv = (arrayOf("konan") + args).map { it.cstr.getPointer(memScope) }.toCValues()

        autoreleasepool {
            UIApplicationMain(argc, argv, null, NSStringFromClass(AppDelegate))
        }
    }
}

@ExportObjCClass
class AppDelegate : UIResponder(), UIApplicationDelegateProtocol {
    companion object : UIResponderMeta(), UIApplicationDelegateProtocolMeta {}

    override fun init() = initBy(AppDelegate())

    override fun application(application: UIApplication, didFinishLaunchingWithOptions: NSDictionary?): Boolean {
        println("Hello Kotlin/Native")
        return true
    }

    private var _window: UIWindow? = null
    override fun window() = _window
    override fun setWindow(window: UIWindow?) { _window = window }
}

@ExportObjCClass
class ViewController : UIViewController {

    constructor(aDecoder: NSCoder) : super(aDecoder)
    override fun initWithCoder(aDecoder: NSCoder) = initBy(ViewController(aDecoder))
}
```

この時点で実行すると、iOS上には白い画面しか出てきませんが、標準出力に"Hello Kotlin/Native"と出力されます。(Xcodeから確認できます)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/15121/c1a0105a-1066-19a6-5b78-a3fff2da3201.png)

## GUI操作
現在はただの白い画面しか見えないため、テキストを表示するようにします。

1. `Main.storyboard`にLabelを追加し、`ViewController.m`に`label`という名で[Outlet](https://developer.apple.com/jp/documentation/ToolsLanguages/Conceptual/Xcode_Overview/BuildingaUserInterface/ConnectingObjectstoCode.html)を追加します。  
Labelの位置はConstraintなどで良い感じに設定してください。
2. `main.kt`のうち、ViewControllerクラスの部分を以下のように修正します。

```kotlin:main.kt
...

@ExportObjCClass
class ViewController : UIViewController {
    private val formatter: NSDateFormatter = NSDateFormatter()

    @ObjCOutlet lateinit var label: UILabel

    constructor(aDecoder: NSCoder) : super(aDecoder) {
        formatter.dateFormat = NSDateFormatter.dateFormatFromTemplate("HH:mm:ss", options = 0, locale = NSLocale("ja_JP")) ?: "HH:mm:ss"
    }
    override fun initWithCoder(aDecoder: NSCoder) = initBy(ViewController(aDecoder))

    override fun viewDidLoad() {
        val timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target = this, selector = NSSelectorFromString("update:"), userInfo = null, repeats = true)
        timer.fire()
    }

    @ObjCAction
    fun update(timer: NSTimer?) {
        label.text = "Current time\n${formatter.stringFromDate(NSDate())}"
    }
}
```

### 結果
![ipad_clock.gif](https://qiita-image-store.s3.amazonaws.com/0/15121/96f5cb4d-de93-3fa3-a692-5709a021d09b.gif)
※ Storyboardでフォントなどの設定をした後の表示です。

## ObjCのライブラリを使う
ObjCのネットワーキングライブラリのド定番である[AFNetworking](https://github.com/AFNetworking/AFNetworking)を例に、ライブラリを追加する方法を記します。

### CocoaPodsでライブラリを追加
ルートディレクトリにPodfileを配置します。

```ruby:Podfile
project '(プロジェクト名).xcodeproj'
platform :ios, '9.0'

target '(プロジェクト名)' do
    use_frameworks!
    pod 'AFNetworking', '~> 3.0'
end
```
podコマンドでライブラリをインストールします。

```bash
$ pod install
```

### ワークスペースをXcodeで開く
今までは単体のプロジェクトをXcodeで開いていましたが、CocoaPodsを用いたプロジェクトではワークスペースを用います。
`pod install`を行うと、ルートディレクトリに`(プロジェクト名).xcworkspace`ができあがっているのでそちらをXcodeで開きます。

Manage SchemesでAFNetworkingを表示します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/15121/66912ab6-a083-fad4-666c-b963013b8e8d.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/15121/e4873de6-2426-8ef3-f9ed-886eeb2df9c6.png)

SchemeをAFNetworking、ビルドターゲットをGeneric iOS Deviceにし、`Product` > `Archive`でアーカイブを作成します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/15121/8f0266b1-459f-1c49-33a3-23ba05e6f515.png)

PodsプロジェクトのProducts内にAFNetworking.frameworkができているので、Show in Finderで保存先ディレクトリを開き、プロジェクトのFrameworksディレクトリにコピーします。

```
root
  ├── Frameworks
  │    └── AFNetworking.framework
  ︙
```

### Kotlin/Nativeでライブラリを使用する
`src/main/c_interop`ディレクトリを作成し、ライブラリ情報を記述した`libs.def`ファイルを配置します。

```text:libs.def
headers = AFNetworking/AFNetworking.h
headerFilter = AFNetworking/**
language = Objective-C
linkerOpts = -framework AFNetworking
compilerOpts = -framework AFNetworking
```

`CMakeLists.txt`, `build.gradle`をそれぞれ以下のように書き換えます。

```diff:CMakeLists.txt
...
  
  project("iOS Kotlin Native Sample" Kotlin)
+ 
+ cinterop(
+         NAME libs
+         TARGET iphone
+         DEF_FILE src/main/c_interop/libs.def
+ )
  
  konanc_executable(
          NAME ios-kotlin-native-sample
          TARGET iphone
-         SOURCES src/main/kotlin/
+         SOURCES src/main/kotlin/ build/konan/libs/iphone  # この行は本来は修正が不要かもしれないがバグがあるようなので追加
  )
```

```diff:build.gradle
...
  konan.targets = ["iphone"]
  
  konanArtifacts {
-     program("app")
+     def productsDir = new File("Frameworks").absolutePath
+     interop("libs") {
+         defFile "src/main/c_interop/libs.def"
+ 
+         compilerOpts "-F${productsDir}"
+         linkerOpts "-F${productsDir}"
+
+         includeDirs new File(".").getAbsolutePath()
+     }
+ 
+     program("app") {
+         linkerOpts '-rpath', '@executable_path/Frameworks'
+ 
+         linkerOpts "-F${productsDir}"
+ 
+         libraries {
+             artifact "libs"
+         }
+     }
  }
```

### cinteropのコンパイル
以下のコマンドを実行します。
```bash
$ ./gradlew compileKonanLibs
```

この時点で`build/konan/libs/iphone/libs.klib-build/kotlin/libs/libs.kt`というファイルが作成されます。このファイルを読み込むように設定したので、ObjCのライブラリを使用可能になります。

# Kotlin/NativeでiOSアプリを書いた所感、まとめ
## 面倒くさい
まだプレビュー版なので、手作業ですることが多いです。ここらへんはそのうち改善されると思います。

## Swfit 2系まで戻った気分になる
Swift 3系から短くなったメソッド名ですが、そちらの名前はサポートされていないので、従来の長い名称を書く必要があります。

## ObjCのファイルを残す必要あり？
サンプルなどではObjCのファイルが残っているので、想像ですが多分Xcodeとの兼ね合いで必要なんだと思います。
なお`ViewController.m`に追加したOutletの記述を削除しても、コンパイル・動作には影響ありません。

## Swiftは現状非対応
ObjCで書かれたライブラリは用いることが可能ですが、Swiftのものは現状手軽には使えないと思います。（補完を考えなければ使えるかもしれませんが）

## 結構重い
現状CLion+Kotlin/NativeはXcode+Swiftより重いです。今後に期待しましょう。

## CLionがObjCに非対応
AppCodeはObjCに対応していますが、CLionは対応していないので、ソースのシンタックスハイライトが残念です。
ちなみにSwiftには対応しています。現状Kotlin/Nativeとの共存は難しいですが。

## Kotlinの標準メソッドを使える
Kotlinの標準的なメソッドは使えます。またStringとNSStringなど部分的に互換性を持つものもあります。
ただし[ハマりポイント](https://qiita.com/noripi/items/4ee969c48b3da5ca6fbd#kotlinnative%E3%81%A7ios%E3%82%92%E6%9B%B8%E3%81%8F%E3%81%A8%E3%81%8D%E3%81%AB%E6%B3%A8%E6%84%8F%E3%81%97%E3%81%9F%E3%81%84%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88)も多そうなので、これも今後に期待ですね。

## まとめ
最後に否定的なことを多く書きましたが、v0.3時代ではできなかったObjCの相互運用が可能になったことで、Kotlin/Nativeの将来的なユースケースは大幅に広がったと思います。

さらにCとの相互運用に比べると、非常にKotlinらしい書き方ができるようになっています。

今後の発展に期待しましょう！