iOS App with Kotlin/Native: Getting Started

0
14

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:

  1. Kotlin/JS for JavaScript Development.
  2. 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.

Kcode build phase

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:

  1. The plugins block contains the id and version of the Kotlin/Native Gradle plugin that you will use.
  2. Saves the absolute path of this project in the productsDir variable, which you will use later.
  3. Lists the target iOS architectures. ios_x64 is the architecture used for the iOS simulators. There are many other targets, for example ios_arm64, which targets a physical iOS device.
  4. 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 the FRAMEWORK output kind.
  5. In the allTargets block, you are passing linker options to the compiler.
  6. 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:

Xcode Objects Library

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.

Fix 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.

Fix missing constraints

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:

  1. Imports for Kotlin to interop with Objective-C and some of the Cocoa Touch frameworks.
  2. The class inherits from UIVIewController and conforms to MKMapViewDelegateProtocol. The @ExportObjCClass annotation helps Kotlin create a class that is visible for lookup at runtime.
  3. Overrides the UIViewController initializer with a Kotlin constructor.
  4. Overrides the viewDidLoad() method.

Hold on a second! Something is missing, here. Where is the syntax highlighting and code completions?

Sad face

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.

AppCode plugin from home screen

Now, click on Install JetBrains plugin and then search for kotlin native. The results should include the Kotlin/Native for AppCode plugin:

Kotlin Native 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:

  1. Create center, span and region properties that will be used to position the viewable area of mapView.
  2. Use the Kotlin standard library with function, to scope and setup a couple of the mapView properties. Inside the scope, you set the mapView delegate equal to this MeteoriteMapViewController and set the region of the mapView.

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’.

create function

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:

  1. Create an MKPointAnnotation that will represent a pin on the map. For now, you set up some mock data.
  2. Add the annotation variable to the mapView. 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.

ViewController checkpoint

Build the project to make sure no errors are showing, by selecting Run from the toolbar and then Build.

AppCode 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:

ViewController focus

Next, select the Identity Inspector and enter MeteoriteMapViewController in the Class field.

Identity Inspector

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.

Open As Source

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’s mapView. For this mapping to work, you also needed to give a hint that mapView can be used as an outlet, which you did with the @ObjCOutlet annotation in the ViewController.
  • destination points to the id of the outlet the mapView should be connected to. Usually, these ids are randomly generated by Xcode when connecting an outlet to a property defined in a ViewController. Under the <subViews> section, find the <mapView> tag and look for its id attribute. This is the id to use in the destination 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.

Running in Xcode:
Run in Xcode

Running in AppCode:
Run in AppCode

After you build and run, the simulator will show your mock Meteorite on the map.

Simulator first run

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.

AppCode new group

AppCode new group name

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 and linkerOpts 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:

  1. The dependencies block lists the libraries the app depends on.
  2. cinterop is the block that will call into the cinterop tool. Passing a string as in cinterop('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.
  3. In includeDirs, you let cinterop search for header files in AFNetworking.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:

  1. Create a class that models the JSON data that you will receive.
  2. Add several properties that use Map Delegation from Kotlin to get their values. The name of each property is the key in the Map.
  3. 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:

  1. NSURL.URLWithString creates an NSURL object to make requests. The params passed in will limit our responses to around a 300-mile radius of Arlington, VA.
  2. AFHTTPSessionManager.manager() is your first call to the AFNetworking framework.
  3. Set the manager to pass all responses back to the app as JSON using AFJSONResponseSerializer.
  4. Invoke a GET request on the manager. You passed in the absolute url, two null values, and a lambda block to handle a successful or failed response, respectively.
  5. 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.
  6. 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.

Simulator with Data

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!

LEAVE A REPLY

Please enter your comment!
Please enter your name here