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! By Eric Crawford.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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.

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.

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.

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.

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.