iOS App with Kotlin/Native: Getting Started
In this tutorial, you’ll build an iOS app using Kotlin/Native. You’ll also take a look at the AppCode IDE from JetBrains!
Version
- Kotlin 1.3, iOS 12, Xcode 10

Kotlin has seen tremendous growth on the Java Virtual Machine (JVM). Having roughly 100% interoperability with Java allows developers to work without giving up the rich ecosystem of libraries and tools. But Java developers are not the only ones who can benefit from the language; Kotlin has spread its tentacles outside the JVM with two other variations:
- Kotlin/JS for JavaScript Development.
- Kotlin/Native for C and iOS Development.
Kotlin/Native comes with a friendly API for the iOS platform, giving developers the ability to call pretty much all of the Cocoa Touch frameworks. This means that Kotlin developers can transfer their development skills over to creating iOS apps (assuming you have a spare macOS device lying around).
In this tutorial, you will build a map-based iOS app that displays where meteorites have fallen to earth. The entire app will focus on using Kotlin/Native. We’ll keep the references to iOS classes and frameworks light in this tutorial, giving you just a taste of the possibilities without overwhelming you if you’ve never developed for iOS.
Note: This tutorial assumes that you are already familiar with the basics of Kotlin programming. If you are new to Kotlin, check out our Kotlin For Android tutorial. If you are new to Android development, check out our introductory Android tutorials first. If you’re ready to learn iOS development, check out our path to learning iOS guide on the site.
Getting Started
Prerequisites: This tutorial utilizes Kotlin/Native 0.9.3, Kotlin 1.3.0, Xcode 10.0 (CLI) and iOS 12.0. The available versions may be higher by the time you read this tutorial.
As a first step, download the materials for this tutorial by clicking the Download materials button at the top or bottom of the page. Once you’ve downloaded the materials, open the starter project in Xcode by double-clicking on the MeteoriteFinder.xcodeproj
file, and then run the app by clicking on the triangle button, shown below:
Since this is the first time running the app, the Kotlin/Native compiler, which is named Konan, will download itself. Depending on your network speed, this can take 5-20 minutes or so. Subsequent builds of the app will be much faster. When the download is done and the build completes, Xcode will start the simulator and you will be greeted by a basic Hello screen.
Nice! You’ve just run your first iOS app from Kotlin, without any Objective-C or Swift source code in this project.
Now that you have the project running, it’s helpful to tour some elements of the project.
Using Kotlin/Native
It may seem magical to push a button and have Kotlin code running on an iOS device. But there are several tools and scripts behind the scenes that help pull off this illusion.
As stated earlier, the Kotlin/Native platform has its own compiler, but manually running it every time you want to build your project would not be an efficient use of time. The the Kotlin team has you covered with Gradle.
Kotlin/Native uses the Gradle Build Tool to automate the entire build process of Kotlin/Native within Xcode. The use of Gradle here means the developer can take advantage of its internal incremental build architecture, only building and downloading what’s needed, saving a developer’s precious time.
Build Phase Script
This is done from the Build Phases panel in Xcode, which you can reach by clicking on the MeteoriteFinder project ▸ MeteoriteFinder under TARGETS ▸ Build Phases. Then, expand Compile Kotlin/Native.
The script calls the gradlew tool that is part of the Gradle Build System and passes in the build environment and debugging options. Then, gradlew invokes a custom task called copyExecutable
, which copies the artifacts that gradlew builds (a .kexe file) to the iOS project build directory. Finally, the script includes the artifact in the iOS executable.
Gradle Build Script
Gradle uses a build script (named build.gradle
) to configure your project. You can find the build.gradle
file under the Supporting Files folder in Xcode.
Look at the build.gradle
file:
// 1
plugins {
id "org.jetbrains.kotlin.platform.native" version "1.3.0"
}
components.main {
// 2
def productsDir = new File("").absolutePath
// 3
targets = ['ios_arm64', 'ios_x64']
// 4
outputKinds = [EXECUTABLE]
// 5
allTargets {
linkerOpts '-rpath', '@executable_path/Frameworks'
linkerOpts "-F${productsDir}"
}
}
// 6
task copyExecutable() {
doLast {
def srcFile = tasks['compileDebugIos_x64KotlinNative'].outputFile
def targetDir = getProperty("konan.configuration.build.dir")
copy {
from srcFile.parent
into targetDir
}
}
}
In more detail:
- The
plugins
block contains theid
andversion
of the Kotlin/Native Gradle plugin that you will use. - Saves the absolute path of this project in the
productsDir
variable, which you will use later. - Lists the target iOS architectures.
ios_x64
is the architecture used for the iOS simulators. There are many other targets, for exampleios_arm64
, which targets a physical iOS device. -
outputKinds
is a list of artifacts that you would like to produce.EXECUTABLE
will produce a standalone executable that can be used to run the app. If you wanted to create a framework, you would use theFRAMEWORK
output kind. - In the
allTargets
block, you are passing linker options to the compiler. - The
copyExecutable
block is the task that you saw in the Build Phases section of Xcode. This script copies the generated executable from the Gradle build folder to the iOS build folder.
Necessary iOS Classes
The Build Phases script and Gradle script are required for building and loading the Kotlin aspect of the project.
There are a couple of files that any iOS project must have. In Xcode, there is the following folder structure: MeteoriteFinder project ▸ MeteoriteFinder ▸ src ▸ main ▸ kotlin. Expand these folders, and you will see two files:
- AppDelegate.kt: Works alongside the app object to ensure your app interacts properly with the system and with other apps.
- main.kt: Just holds the main method to start the app.
You won’t be touching AppDelegate.kt or main.kt, but it is good to know that you have these Kotlin files to run the app.
Now that you know how the project is built, time to start finding some Meteorites!
Adding a MKMapView to the Storyboard
First you’ll add a MKMapview component to the storyboard. This will show the locations of the Meteorites. Open Main.storyboard and delete the label hello.
Note: You may or may not have heard of a storyboard when it comes to iOS development. If you haven’t, just think of it as a file used to visually create your app’s layout.
In the Xcode toolbar, click on the Library button. When a pop-up appears, search for Map Kit View (which corresponds to MKMapView) like below:
Next, drag the Map Kit View to the phone scene where you just deleted the label. Then, drag the edges of the Map Kit View to take up the full screen.
In order to show the map correctly on different device screen sizes, you’ll need to adjust the constraints. Since you only have one view to worry about, Xcode will set the constraints for you.
Select the MKMapview component you added. Then, click on the Resolve Auto Layout Issues button on the Xcode bottom toolbar. After that, select Add Missing Constraints.
Now that the Map Kit View scene has been set in the storyboard with some constraints, you’ll next add a ViewController.
Creating the ViewController
ViewControllers are the heart of most iOS apps. They are similar to Activities on Android.
You are going to create a ViewController in Kotlin. Since Xcode does not understand Kotlin source code, you will have to create an empty file for your ViewController.
Right-click on the kotlin folder and select New File…. In the template window, scroll down to Other section and select Empty and click Next. Name the file MeteoriteMapViewController.kt, then click Create.
Add the following code to the new file:
// 1
import kotlinx.cinterop.ExportObjCClass
import kotlinx.cinterop.ObjCObjectBase.OverrideInit
import kotlinx.cinterop.ObjCOutlet
import platform.Foundation.*
import platform.UIKit.*
import platform.MapKit.*
// 2
@ExportObjCClass
class MeteoriteMapViewController : UIViewController, MKMapViewDelegateProtocol {
// 3
@OverrideInit
constructor(coder: NSCoder) : super(coder)
// 4
override fun viewDidLoad() {
super.viewDidLoad()
}
}
Here is what you added:
- Imports for Kotlin to interop with Objective-C and some of the Cocoa Touch frameworks.
- The class inherits from
UIVIewController
and conforms toMKMapViewDelegateProtocol
. The@ExportObjCClass
annotation helps Kotlin create a class that is visible for lookup at runtime. - Overrides the
UIViewController
initializer with a Kotlin constructor. - Overrides the
viewDidLoad()
method.
Hold on a second! Something is missing, here. Where is the syntax highlighting and code completions?
Luckily, the creators of Kotlin can help you out here via the AppCode IDE.
Using AppCode for Kotlin/Native
AppCode is JetBrains dedicated iOS development IDE. It supports Objective-C, Swift and our friend Kotlin (with a plugin). Head over to the AppCode download site to download and install the latest stable version.
Note: Unfortunately AppCode is not free. There is a 30-day trial of the production version.
With AppCode installed, you need to install the Kotlin/Native IDE plugin. Open AppCode, then click Configure ▸ Plugins to show the plugins window.
Now, click on Install JetBrains plugin and then search for kotlin native. The results should include the Kotlin/Native for AppCode plugin:
Go ahead and click Install to install the plugin, and when it’s done installing, restart AppCode. Once restarted, select Open Project and navigate to the root folder of your MeteoriteFinder project (the one that contains the MeteoriteFinder.xcodeproj file) and click Open. Once the project is open, you can expand the folder structure and explore the layout. If you already use Android Studio or IntelliJ IDEA, AppCode will look very familiar.
Fleshing out the ViewController
With AppCode open and ready, it’s time to get back to the project. Double-click on MeteoriteMapViewController.kt
and bask in the syntax highlighting provided by the Kotlin/Native plugin and all its glory.
Note: At this point, you may see red underlining under super(coder)
. This is a false positive, and you may see more through out this tutorial. The Kotlin/Native plugin is in active development and is still in beta. Valid errors will be caught by the compiler when you build/run the project. Also, full syntax highlighting may not show up immediately, but instead only after a short time, once AppCode has fully integrated the Kotlin/Native plugin. The syntax highlighting may also intermittantly not show up fully, due to the beta nature of the plugin.
Under the constructor, add the following:
@ObjCOutlet
lateinit var mapView: MKMapView
The @ObjCOutlet
annotation sets the mapView
property as an outlet. This allows you to link the MKMapview from the storyboard to this property. The mapView
property will be initialized at a later time than the ViewController is created, so you use the lateinit
keyword.
In the viewDidLoad()
method, add the following under super.viewDidLoad()
:
// 1
val center = CLLocationCoordinate2DMake(38.8935754, -77.0847873)
val span = MKCoordinateSpanMake(0.7, 0.7)
val region = MKCoordinateRegionMake(center, span)
// 2
with(mapView){
delegate = this@MeteoriteMapViewController
setRegion(region, true)
}
Going through this, step by step:
- Create
center
,span
andregion
properties that will be used to position the viewable area ofmapView
. - Use the Kotlin standard library
with
function, to scope and setup a couple of themapView
properties. Inside the scope, you set themapView
delegate equal to thisMeteoriteMapViewController
and set the region of themapView
.
CLLocationCoordinate2DMake
is from a different module, and you will need to import from the CoreLocation module to make the compiler happy. You can write the import at the top of the file:
import platform.CoreLocation.CLLocationCoordinate2DMake
Or instead you can let the IDE add the import for you by setting your cursor on CLLocationCoordinate2DMake
and hitting option+return at the same time on your keyboard.
Since MeteoriteMapViewController
conforms to MKMapViewDelegateProtocol
, setting the mapView
delegate to this class allows MeteoriteMapViewController
to receive callback events from mapView
.
To conform to the protocol, first implement the method mapViewDidFailLoadingMap()
, just in case the map fails to load. Add the following under the viewDidLoad()
method:
override fun mapViewDidFailLoadingMap(mapView: MKMapView, withError: NSError) {
NSLog("Error loading map: $withError")
}
Next, you’ll create a method that will insert mock data to be displayed on the map. Add the following method call to the end of viewDidLoad()
:
createAnnotation()
The method call should display red because it isn’t declared yet. Time to do so! Select the method, then press option+return at the same time. In the context menu, select Create function ‘createAnnotation’.
Inside the new createAnnotation()
method, delete the TODO
template code and add the following:
// 1
val annotation = MKPointAnnotation().apply {
setCoordinate(CLLocationCoordinate2DMake(38.8935754, -77.0847873))
setTitle("My mock meteorite")
setSubtitle("I'm falling........")
}
// 2
mapView.addAnnotation(annotation)
In the above, you:
- Create an
MKPointAnnotation
that will represent a pin on the map. For now, you set up some mock data. - Add the
annotation
variable to themapView
. This will add a mock data pin on the map.
At this point, you’ve created a ViewController that will display a single pin on a map.
Build the project to make sure no errors are showing, by selecting Run from the toolbar and then Build.
The first build in AppCode may take a little time to complete.
Wiring up the Storyboard
Congrats!! No build errors.
Next, you need to connect the ViewController to the layout defined in the storyboard. This means you need to add a reference to your ViewController in the storyboard.
AppCode does not support editing storyboards, so this task will need to be done in Xcode. Double-click on Main.storyboard
to open up in Xcode IDE. Then, click on the ViewController icon above the simulated phone:
Next, select the Identity Inspector and enter MeteoriteMapViewController in the Class field.
Finally, you will connect the MKMapView view from the storyboard to the mapView
property in the MeteoriteMapViewController class.
Note: In Objective-C and Swift files, Xcode allows you to drag a line from the storyboard view directly to the code and make the connection, automatically. But since Xcode does not understand Kotlin, you will need to do this task manually in XML.
In Xcode, right-click on Main.storyboard
then select Open As ▸ Source Code.
In the XML, find the closing </viewController>
tag and add the following connection right above it:
<connections>
<outlet property="mapView" destination="<Your mapView ID>" id="rPX-AH-rma"/>
</connections>
The above code shows how the storyboard knows what views belong to your outlets. The attributes in the outlet
tag do all the mapping.
-
property
points to the name of the actual property in your code. In this case, it’smapView
. For this mapping to work, you also needed to give a hint thatmapView
can be used as an outlet, which you did with the@ObjCOutlet
annotation in the ViewController. -
destination
points to the id of the outlet themapView
should be connected to. Usually, these ids are randomly generated by Xcode when connecting an outlet to a property defined in aViewController
. Under the<subViews>
section, find the<mapView>
tag and look for itsid
attribute. This is the id to use in thedestination
attribute. -
id
is a randomly generated id. You will not be using this directly.
Note: To return to the storyboard layout, right-click on Main.storyboard
, then select Open As ▸ Interface Builder – Storyboard. Also, Xcode is only being used for editing the storyboard. Feel free to close Xcode before moving to the next section.
With those changes, you can now run the app from either Xcode or AppCode.
After you build and run, the simulator will show your mock Meteorite on the map.
Adding an Objective-C Third-Party Library
Now that you have one mock data pin showing on the map, how about livening up the map with some real data?
Thanks to NASA, you have access to a rich collection of historical meteorite data. You can view the tabular format here, but your app will be consuming the JSON API located here.
Now that you can locate historical meteorite data, how do you do a network call to get it? You will use a popular third-party Objective-C library called AFNetworking. The framework is already included in the project as AFNetworking.framework
; you just need to make it Kotlin friendly.
Kotlin/Native provides headers for the iOS platform frameworks out of the box, but how can you call third-party iOS libraries?
Creating your own headers for interoperability is done via a tool called cinterop. This tool converts C and Objective-C headers into a Kotlin API that your code can use.
Before running this tool, you will need to create a .def file that details what frameworks to convert and how to convert them. Return to AppCode. Right-click on the main folder, then select New ▸ Group. Name the folder c_interop.
Note: This is the default location that the cinterop tool will look for def files.
Next, right-click on the c_interop folder and select New ▸ File to create a new file. Name the file afnetworking.def. Add the following to the file:
language = Objective-C
headers = AFURLSessionManager.h AFURLResponseSerialization.h AFHTTPSessionManager.h
compilerOpts = -framework AFNetworking
linkerOpts = -framework AFNetworking
Going through the snippet above:
-
language
informs the tool of the framework language. -
headers
is the list of headers to generate Kotlin API’s. Here, you set three headers that your app will use from AFNetworking. -
compilerOpts
andlinkerOpts
are compiler and linker options that will be passed to the tool.
That’s all the configuration needed to run the cinterop tool. Gradle provides support that will allow you to automate the cinterop tasks when you build the project.
Open build.gradle
and at the end of the components.main
section add the following:
// 1
dependencies {
// 2
cinterop('AFNetworking'){
packageName 'com.afnetworking'
compilerOpts "-F${productsDir}"
linkerOpts "-F${productsDir}"
// 3
includeDirs{
allHeaders "${productsDir}/AFNetworking.framework/Headers"
}
}
}
Reviewing this in turn:
- The
dependencies
block lists the libraries the app depends on. -
cinterop
is the block that will call into the cinterop tool. Passing a string as incinterop('AFNetworking')
will use AFNetworking to name the Gradle tasks and the generated Kotlin file. You also give the library a package name so that the code is namespaced. Finally, you pass in compiler and linker options which define where to find the library. - In
includeDirs
, you let cinterop search for header files inAFNetworking.framework/Headers
.
The next time that you build the project, the cinterop tool will create a Kotlin friendly API around the AFNetworking library that your project can use.
Making Network Requests
With AFNetworking ready to make network calls on your behalf, you can start pulling in some real data. But first, you will create a model for the data. Under the kotlin folder, create a new file named Meteorite.kt and add the following:
// 1
class Meteorite(val json:Map<String, Any?>) {
// 2
val name:String by json
val fall:String by json
val reclat:String by json
val reclong:String by json
val mass:String by json
val year:String by json
// 3
companion object {
fun fromJsonList(jsonList:List<HashMap<String, Any>>):List<Meteorite> {
val meteoriteList = mutableListOf<Meteorite>()
for (jsonObject in jsonList) {
val newMeteorite = Meteorite(jsonObject)
if (newMeteorite.name != null
&& newMeteorite.fall != null
&& newMeteorite.reclat != null
&& newMeteorite.reclong != null
&& newMeteorite.mass != null
&& newMeteorite.year != null) {
meteoriteList.add(newMeteorite)
}
}
return meteoriteList
}
}
}
Reviewing the code above:
- Create a
class
that models the JSON data that you will receive. - Add several properties that use Map Delegation from Kotlin to get their values. The name of each property is the key in the Map.
- Add
fromJsonList()
inside of a companion object so that it can be called on the class type. This function takes a list of map objects and returns a list of valid Meteorite objects. A valid Meteorite object is one wherein none of the properties are null.
You’ll now set up a network request to retrieve real meteorite data. Go back to MeteoriteMapViewController.kt
. Start by importing the AFNetworking package so that you can use it in the class:
import com.afnetworking.*
Next, under the mapView
property declaration add properties to hold collections of Meteorite and MKPointAnnotation objects:
var meteoriteList = listOf<Meteorite>()
val meteoriteAnnotations = mutableListOf<MKPointAnnotation>()
Then add the following method that will load the data:
private fun loadData() {
val baseURL = "https://data.nasa.gov/"
val path = "resource/y77d-th95.json"
val params = "?\$where=within_circle(GeoLocation,38.8935754,-77.0847873,500000)"
// 1
val url = NSURL.URLWithString("$baseURL$path$params")
// 2
val manager = AFHTTPSessionManager.manager()
// 3
manager.responseSerializer = AFJSONResponseSerializer.serializer()
// 4
manager.GET(url?.absoluteString!!, null, null, { _:NSURLSessionDataTask?, responseObject:Any? ->
// 5
val listOfObjects = responseObject as? List<HashMap<String, Any>>
listOfObjects?.let {
meteoriteList = Meteorite.fromJsonList(it)
for (meteorite in meteoriteList) {
meteoriteAnnotations.add(createAnnotation(meteorite))
}
mapView.addAnnotations(meteoriteAnnotations)
}
}, { _:NSURLSessionDataTask?, error:NSError? ->
// 6
NSLog("Got a error ${error}")
})
}
Unpacking the snippet part by part:
-
NSURL.URLWithString
creates anNSURL
object to make requests. Theparams
passed in will limit our responses to around a 300-mile radius of Arlington, VA. -
AFHTTPSessionManager.manager()
is your first call to the AFNetworking framework. - Set the
manager
to pass all responses back to the app as JSON using AFJSONResponseSerializer. - Invoke a
GET
request on themanager
. You passed in the absolute url, two null values, and a lambda block to handle a successful or failed response, respectively. - Successful responses are returned in this lambda. The response is cast into a list of HashMaps. Then, that list is converted into a list of Meteorite. Finally, create map annotations for each Meteorite and add it to the
mapView
. - This lambda will be called if there are any networking errors; you’re just logging the error.
Finally, change the call from createAnnotation()
in the viewDidLoad()
method to instead be to loadData()
, and update the method createAnnotation()
to be the following:
private fun createAnnotation(meteorite:Meteorite) = MKPointAnnotation().apply {
val latitude = meteorite.reclat.toDouble()
val longitude = meteorite.reclong.toDouble()
setCoordinate(CLLocationCoordinate2DMake(latitude, longitude))
setTitle(meteorite.name)
setSubtitle("Fell in ${meteorite.year.substringBefore("-")}" +
" with a mass of ${meteorite.mass} grams")
}
With these changes, you’re passing Meteorite objects and dynamically adding pins to the map using MKPointAnnotation
. You’re also using Kotlin’s Single-Expression Function format combined with the apply
function to ease the process of instantiating MKPointAnnotation
objects and populating their values.
Build and run the app again, then get ready to begin your quest to discover the fallen meteorites.
To zoom in/out in the iOS simulator, hold down the Option key and drag across the map.
Note: As stated earlier, Kotlin/Native is in beta and there are still some rough edges. If you are still seeing your mock meteorite, you may need to delete the build folder to force a clean build of the project.
Where to Go From Here?
Download the fully finished projects using the Download materials button at the top or bottom of this tutorial to see how it went.
Kotlin/Native has opened the doors to interoperability with more platforms. You’ve seen how it works on iOS, but you can also target platforms like Linux with C and Web with WebAssembly. You can even target both Android and iOS in the same project using Kotlin Multi-Platform Modules.
In this tutorial, you have only scratched the surface of how Kotlin/Native works on iOS and what it offers. More information can be found on the Kotlin website and on Github.
Please add your thoughts, comments, and questions to the discussion below, and have fun exploring Kotlin/Native!
Comments