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", "john@example.com"),
User(2, "jane_doe", "jane@example.com")
)

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", "john@remote.com"),
User(2, "remote_jane", "jane@remote.com")
)
}
}

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 feedback@compilenrun.com. I’d greatly appreciate your feedback! :)