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:
- Go to File > New > New Project
- Select "Kotlin Multiplatform App" and click Next
- Configure your project with:
- Name:
MyKmpApp
- Package name:
com.example.mykmpapp
- Shared Module name:
shared
- Name:
// 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 SwiftUIshared
: Common Kotlin code that will be shared between platforms
Let's look at the shared
module's build script:
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:
commonMain
: Contains code shared between Android and iOSandroidMain
: Contains Android-specific implementationsiosMain
: 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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
-
Keep platform-specific code minimal: Focus on sharing business logic, networking, and data manipulation.
-
Use proper architecture: Follow Clean Architecture or MVVM to make code more maintainable and shareable.
-
Plan for testability: Write unit tests for your shared code.
-
Handle memory appropriately: Be aware of memory management differences between Kotlin and Swift.
-
Expose simple interfaces: Make your shared API clean and straightforward for Swift to consume.
-
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
// 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
// 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
// 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
// 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:
- Setting up a Kotlin Multiplatform project
- Understanding the project structure
- Writing shared code with the
expect/actual
pattern - Exposing Kotlin code to Swift
- Handling concurrency across platforms
- Integrating with iOS UI frameworks
- Building a real-world weather app example
Additional Resources
- Official Kotlin Multiplatform Documentation
- KMP Mobile Portal
- Kotlin/Native Interop with Swift and Objective-C
- KMM Sample Apps
Exercises
-
Basic: Create a simple to-do list app that shares data models and repository logic between Android and iOS.
-
Intermediate: Extend the weather app to include a 5-day forecast and location search.
-
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! :)