はじめに
この記事はエムスリーAdvent Calendarの19日目の記事です。昨日は@maeharinによるサーバーサイドKotlinについての話でした。
私はAndroidをメインとしたアプリエンジニアなので、本日はなぜかKotlin Advent Calendarでも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
ただし、バージョンアップにより上の発表時点との差異が出てきています。大きな変更点を簡単に挙げると、
- Gradleファイルの書き方が変わった
- CMakeでもビルド可能になった
- 割とまともにiOSアプリを書けるようになった
という感じです。3番目はKotlinConfのアプリが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での作業
今回は単一のプロジェクトしか使わないので、ワークスペースは作成しません。
- Xcodeで新しいプロジェクトを作成します。
- プロジェクトの種類はSingle View App、言語はObjective-Cを選択します。
- プロジェクトの設定でBuild Phasesに以下のような
Run Script Phase
を2つ追加します。適宜名前をつけることをお薦めします。
./gradlew -p $SRCROOT compileKonanApp
cp "$SRCROOT/build/konan/bin/iphone/app.kexe" "$TARGET_BUILD_DIR/$EXECUTABLE_PATH"
- 自動的に生成されたObjCなどのファイルはそのままにしておきます。
CLionでの作業
以下の作業は、Kotlin/Native関連のCMake関連ファイルをコピーするために行う作業です。既に他のプロジェクトがある場合は、KotlinCMakeModule
ディレクトリと、以下のCMakeLists.txt
をルートにコピーするだけで問題ないです。
CLionの一時的なプロジェクトを作成
Xcodeのプロジェクトを作成したディレクトリとは別の、空のディレクトリに新規プロジェクトを作成してください。
Kotlin/Nativeプラグインを入れていれば、Kotlin/Native Applicationを選択できるはずなので、そちらを選択します。
現在はサンプルからしか選べず、またiOSのサンプルは選べないので、"Hello World"プロジェクトを選択します。
KotlinCMakeModule
の抽出
CLionを終了し、作ったディレクトリからKotlinCMakeModule
ディレクトリをXcodeのプロジェクトを作成したディレクトリにコピーします。CLionで作ったディレクトリはもう不要なので削除してください。
ソースディレクトリの作成
src/main/kotlin
ディレクトリを作成します。
CLionを再度開く
CLionをXcodeのプロジェクトのディレクトリで開きます。今はまだCMakeLists.txt
がないので警告が出るはずです。
CMakeLists.txt
の作成
以下のような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/
)
これでsrc/main/kotlin
ディレクトリ内にKotlinファイルを配置するとIDE支援を受けることができます。
※ ただしめちゃくちゃ重い
Gradleの配置、記述
一般的なGradleプロジェクトと同様にGradle Wrapper (v3.3) を配置します。
また、以下のように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
に書くことにします。
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から確認できます)
GUI操作
現在はただの白い画面しか見えないため、テキストを表示するようにします。
-
Main.storyboard
にLabelを追加し、ViewController.m
にlabel
という名でOutletを追加します。
Labelの位置はConstraintなどで良い感じに設定してください。 -
main.kt
のうち、ViewControllerクラスの部分を以下のように修正します。
...
@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())}"
}
}
結果
※ Storyboardでフォントなどの設定をした後の表示です。
ObjCのライブラリを使う
ObjCのネットワーキングライブラリのド定番であるAFNetworkingを例に、ライブラリを追加する方法を記します。
CocoaPodsでライブラリを追加
ルートディレクトリにPodfileを配置します。
project '(プロジェクト名).xcodeproj'
platform :ios, '9.0'
target '(プロジェクト名)' do
use_frameworks!
pod 'AFNetworking', '~> 3.0'
end
podコマンドでライブラリをインストールします。
$ pod install
ワークスペースをXcodeで開く
今までは単体のプロジェクトをXcodeで開いていましたが、CocoaPodsを用いたプロジェクトではワークスペースを用います。
pod install
を行うと、ルートディレクトリに(プロジェクト名).xcworkspace
ができあがっているのでそちらをXcodeで開きます。
Manage SchemesでAFNetworkingを表示します。
SchemeをAFNetworking、ビルドターゲットをGeneric iOS Deviceにし、Product
> Archive
でアーカイブを作成します。
PodsプロジェクトのProducts内にAFNetworking.frameworkができているので、Show in Finderで保存先ディレクトリを開き、プロジェクトのFrameworksディレクトリにコピーします。
root
├── Frameworks
│ └── AFNetworking.framework
︙
Kotlin/Nativeでライブラリを使用する
src/main/c_interop
ディレクトリを作成し、ライブラリ情報を記述したlibs.def
ファイルを配置します。
headers = AFNetworking/AFNetworking.h
headerFilter = AFNetworking/**
language = Objective-C
linkerOpts = -framework AFNetworking
compilerOpts = -framework AFNetworking
CMakeLists.txt
, build.gradle
をそれぞれ以下のように書き換えます。
...
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 # この行は本来は修正が不要かもしれないがバグがあるようなので追加
)
...
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のコンパイル
以下のコマンドを実行します。
$ ./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など部分的に互換性を持つものもあります。
ただしハマりポイントも多そうなので、これも今後に期待ですね。
まとめ
最後に否定的なことを多く書きましたが、v0.3時代ではできなかったObjCの相互運用が可能になったことで、Kotlin/Nativeの将来的なユースケースは大幅に広がったと思います。
さらにCとの相互運用に比べると、非常にKotlinらしい書き方ができるようになっています。
今後の発展に期待しましょう!