Skip to main content

Kotlin iOS Development

Introduction

Kotlin Multiplatform (KMP) allows developers to share code between different platforms, including iOS. This enables you to write business logic, networking, data processing, and other non-UI components once in Kotlin and reuse them across both Android and iOS applications.

In this guide, you'll learn how to set up Kotlin for iOS development, understand the architecture of a KMP project targeting iOS, and see practical examples of code sharing between platforms.

Prerequisites

Before diving into Kotlin iOS development, ensure you have:

  • macOS (required for iOS development)
  • Android Studio
  • Xcode
  • JDK 11 or newer
  • Kotlin Multiplatform plugin

Setting Up Your First Kotlin iOS Project

Step 1: Create a new Kotlin Multiplatform Mobile project

In Android Studio:

  1. Go to File > New > New Project
  2. Select "Kotlin Multiplatform App" and click Next
  3. Configure your project with:
    • Name: MyKmpApp
    • Package name: com.example.mykmpapp
    • Shared Module name: shared
kotlin
// The generated project structure will look like this:
// androidApp/ - Android application
// iosApp/ - iOS application
// shared/ - Shared Kotlin code

Step 2: Understanding the Project Structure

The generated project consists of three main modules:

  • androidApp: Android-specific code (Activities, Fragments, etc.)
  • iosApp: iOS application written in Swift using UIKit or SwiftUI
  • shared: Common Kotlin code that will be shared between platforms

Let's look at the shared module's build script:

kotlin
plugins {
kotlin("multiplatform")
id("com.android.library")
}

kotlin {
android()

listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "shared"
}
}

sourceSets {
val commonMain by getting {
dependencies {
// Common dependencies go here
}
}

val androidMain by getting {
dependencies {
// Android-specific dependencies
}
}

val iosMain by getting {
dependencies {
// iOS-specific dependencies
}
}
}
}

This build script configures three main source sets:

  1. commonMain: Contains code shared between Android and iOS
  2. androidMain: Contains Android-specific implementations
  3. iosMain: Contains iOS-specific implementations

Writing Shared Code for iOS

Creating a Simple Shared Data Class

Let's create a simple data class in the shared module:

kotlin
// In shared/src/commonMain/kotlin/com/example/mykmpapp/data/User.kt
package com.example.mykmpapp.data

data class User(
val id: Int,
val username: String,
val email: String
)

Creating a Shared Repository

Now, let's create a repository that could be used by both platforms:

kotlin
// In shared/src/commonMain/kotlin/com/example/mykmpapp/repository/UserRepository.kt
package com.example.mykmpapp.repository

import com.example.mykmpapp.data.User

interface UserRepository {
fun getUsers(): List<User>
fun getUserById(id: Int): User?
}

class UserRepositoryImpl : UserRepository {
// A simple in-memory database for demonstration
private val users = listOf(
User(1, "john_doe", "[email protected]"),
User(2, "jane_doe", "[email protected]")
)

override fun getUsers(): List<User> {
return users
}

override fun getUserById(id: Int): User? {
return users.find { it.id == id }
}
}

Platform-Specific Implementations

Sometimes you need to implement platform-specific code. You can use the expect/actual pattern for this:

kotlin
// In shared/src/commonMain/kotlin/com/example/mykmpapp/platform/Platform.kt
package com.example.mykmpapp.platform

expect class Platform() {
val platform: String
}

// In shared/src/androidMain/kotlin/com/example/mykmpapp/platform/Platform.kt
package com.example.mykmpapp.platform

actual class Platform actual constructor() {
actual val platform: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}

// In shared/src/iosMain/kotlin/com/example/mykmpapp/platform/Platform.kt
package com.example.mykmpapp.platform

import platform.UIKit.UIDevice

actual class Platform actual constructor() {
actual val platform: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

Using Kotlin Code from Swift

Exposing a Shared Module API

To make your Kotlin code easily accessible from Swift, it's helpful to create a central entry point:

kotlin
// In shared/src/commonMain/kotlin/com/example/mykmpapp/Greeting.kt
package com.example.mykmpapp

import com.example.mykmpapp.platform.Platform
import com.example.mykmpapp.repository.UserRepository
import com.example.mykmpapp.repository.UserRepositoryImpl

class Greeting {
private val platform: Platform = Platform()
private val userRepository: UserRepository = UserRepositoryImpl()

fun greeting(): String {
return "Hello from ${platform.platform}!"
}

fun getUsers() = userRepository.getUsers()

fun getUserById(id: Int) = userRepository.getUserById(id)
}

Accessing Kotlin Code in Swift

In your iOS application, you can now use the shared code like this:

swift
// In iosApp/iosApp/ContentView.swift
import SwiftUI
import shared

struct ContentView: View {
private let greeting = Greeting()
@State private var users: [User] = []

var body: some View {
VStack {
Text(greeting.greeting())
.padding()

List(users, id: \.id) { user in
VStack(alignment: .leading) {
Text(user.username)
.font(.headline)
Text(user.email)
.font(.subheadline)
}
}
.onAppear {
users = greeting.getUsers()
}
}
}
}

Working with Concurrency

Handling asynchronous operations across platforms requires special attention. KMP provides Dispatchers to manage threading:

kotlin
// In shared/src/commonMain/kotlin/com/example/mykmpapp/repository/RemoteUserRepository.kt
package com.example.mykmpapp.repository

import com.example.mykmpapp.data.User
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class RemoteUserRepository {
suspend fun fetchUsers(): List<User> = withContext(Dispatchers.Default) {
// Simulating network request delay
kotlinx.coroutines.delay(1000)

// Return mock data
listOf(
User(1, "remote_john", "[email protected]"),
User(2, "remote_jane", "[email protected]")
)
}
}

To expose this functionality to iOS, we need to handle the coroutines properly:

kotlin
// In shared/src/commonMain/kotlin/com/example/mykmpapp/UserService.kt
package com.example.mykmpapp

import com.example.mykmpapp.data.User
import com.example.mykmpapp.repository.RemoteUserRepository
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch

class UserService {
private val repository = RemoteUserRepository()
private val scope = MainScope()

fun fetchUsers(callback: (List<User>) -> Unit) {
scope.launch {
val users = repository.fetchUsers()
callback(users)
}
}
}

In Swift, you can use this like:

swift
// In iosApp/iosApp/RemoteUsersView.swift
import SwiftUI
import shared

struct RemoteUsersView: View {
private let userService = UserService()
@State private var users: [User] = []
@State private var isLoading = false

var body: some View {
VStack {
if isLoading {
ProgressView()
} else {
List(users, id: \.id) { user in
VStack(alignment: .leading) {
Text(user.username)
.font(.headline)
Text(user.email)
.font(.subheadline)
}
}
}
}
.onAppear(perform: loadUsers)
}

private func loadUsers() {
isLoading = true
userService.fetchUsers { fetchedUsers in
self.users = fetchedUsers
self.isLoading = false
}
}
}

Integrating with iOS UI Frameworks

Using Kotlin with SwiftUI

SwiftUI works well with Kotlin Multiplatform. You can create view models in Kotlin and observe them in SwiftUI:

kotlin
// In shared/src/commonMain/kotlin/com/example/mykmpapp/viewmodel/UserViewModel.kt
package com.example.mykmpapp.viewmodel

import com.example.mykmpapp.data.User
import com.example.mykmpapp.repository.UserRepository
import com.example.mykmpapp.repository.UserRepositoryImpl
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class UserViewModel {
private val repository: UserRepository = UserRepositoryImpl()
private val _users = MutableStateFlow<List<User>>(emptyList())
val users: StateFlow<List<User>> = _users.asStateFlow()

fun loadUsers() {
val loadedUsers = repository.getUsers()
_users.value = loadedUsers
}
}

To use this with SwiftUI, you can create a wrapper:

swift
// In iosApp/iosApp/UserListViewModel.swift
import Foundation
import shared
import Combine

class UserListViewModel: ObservableObject {
private let viewModel = UserViewModel()
@Published var users: [User] = []

init() {
// Set up observation
// Note: This is simplified. In a real app, you'd use a library
// like KMPNativeCoroutinesCombine for proper Flow observation
}

func loadUsers() {
viewModel.loadUsers()
users = viewModel.users.value
}
}

Best Practices for Kotlin iOS Development

  1. Keep platform-specific code minimal: Focus on sharing business logic, networking, and data manipulation.

  2. Use proper architecture: Follow Clean Architecture or MVVM to make code more maintainable and shareable.

  3. Plan for testability: Write unit tests for your shared code.

  4. Handle memory appropriately: Be aware of memory management differences between Kotlin and Swift.

  5. Expose simple interfaces: Make your shared API clean and straightforward for Swift to consume.

  6. Use CocoaPods integration: For larger projects, use CocoaPods to manage your Kotlin dependencies.

Real-world Example: A Weather App

Let's build a simple weather app that demonstrates the concepts we've learned.

Shared Data Model

kotlin
// In shared/src/commonMain/kotlin/com/example/mykmpapp/weather/WeatherData.kt
package com.example.mykmpapp.weather

data class WeatherData(
val city: String,
val temperature: Double,
val description: String,
val humidity: Int
)

Shared Repository and API

kotlin
// In shared/src/commonMain/kotlin/com/example/mykmpapp/weather/WeatherRepository.kt
package com.example.mykmpapp.weather

interface WeatherRepository {
suspend fun getWeatherForCity(cityName: String): WeatherData
}

class MockWeatherRepository : WeatherRepository {
override suspend fun getWeatherForCity(cityName: String): WeatherData {
// Simulate API delay
kotlinx.coroutines.delay(1000)

return WeatherData(
city = cityName,
temperature = 20.0 + (-5..5).random(),
description = listOf("Sunny", "Cloudy", "Rainy", "Windy").random(),
humidity = (30..90).random()
)
}
}

Weather Service for iOS

kotlin
// In shared/src/commonMain/kotlin/com/example/mykmpapp/weather/WeatherService.kt
package com.example.mykmpapp.weather

import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch

class WeatherService {
private val repository: WeatherRepository = MockWeatherRepository()
private val scope = MainScope()

fun getWeatherForCity(city: String, callback: (WeatherData) -> Unit, onError: (String) -> Unit) {
scope.launch {
try {
val weatherData = repository.getWeatherForCity(city)
callback(weatherData)
} catch (e: Exception) {
onError(e.message ?: "An unknown error occurred")
}
}
}
}

Using the Weather Service in SwiftUI

swift
// In iosApp/iosApp/WeatherView.swift
import SwiftUI
import shared

struct WeatherView: View {
private let weatherService = WeatherService()
@State private var city: String = "London"
@State private var weatherData: WeatherData?
@State private var isLoading = false
@State private var errorMessage: String?

var body: some View {
VStack(spacing: 20) {
TextField("City", text: $city)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()

Button("Get Weather") {
fetchWeather()
}
.padding()
.disabled(isLoading)

if isLoading {
ProgressView()
} else if let error = errorMessage {
Text(error)
.foregroundColor(.red)
} else if let weather = weatherData {
WeatherDetailView(weather: weather)
}

Spacer()
}
.padding()
}

private func fetchWeather() {
isLoading = true
errorMessage = nil

weatherService.getWeatherForCity(
city: city,
callback: { data in
weatherData = data
isLoading = false
},
onError: { error in
errorMessage = error
isLoading = false
}
)
}
}

struct WeatherDetailView: View {
let weather: WeatherData

var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text(weather.city)
.font(.largeTitle)

HStack {
Text("\(Int(weather.temperature))°C")
.font(.system(size: 50))

Spacer()

VStack(alignment: .trailing) {
Text(weather.description)
.font(.title2)
Text("Humidity: \(weather.humidity)%")
}
}
}
.padding()
.background(Color.blue.opacity(0.1))
.cornerRadius(10)
.padding()
}
}

Summary

Kotlin Multiplatform offers a powerful way to share code between Android and iOS platforms, allowing you to write business logic, networking, and data processing once and reuse it across both platforms. By leveraging KMP:

  • You can save development time by writing shared code once
  • Maintain consistency between platforms
  • Keep platform-specific code where it belongs
  • Leverage existing Swift and iOS frameworks

In this guide, we've covered:

  1. Setting up a Kotlin Multiplatform project
  2. Understanding the project structure
  3. Writing shared code with the expect/actual pattern
  4. Exposing Kotlin code to Swift
  5. Handling concurrency across platforms
  6. Integrating with iOS UI frameworks
  7. Building a real-world weather app example

Additional Resources

Exercises

  1. Basic: Create a simple to-do list app that shares data models and repository logic between Android and iOS.

  2. Intermediate: Extend the weather app to include a 5-day forecast and location search.

  3. Advanced: Implement a note-taking app that syncs with a backend API, sharing all network code, data models, and business logic between platforms.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)