SwiftUI for iOS in Kotlin Multiplatform

Codefy Labs's photo
·

4 min read

Kotlin Multiplatform (KMP) is a powerful tool that enables developers to write shared code for multiple platforms, including iOS and Android. This approach can significantly reduce development time and effort. When integrating KMP with iOS, one often encounters the challenge of interacting between shared Kotlin code and SwiftUI, Apple's modern UI framework for building user interfaces on iOS. This document explores how shared Kotlin code interacts with SwiftUI, the limitations and challenges involved, and some use cases.

Interaction Between Shared Kotlin Code and SwiftUI

KMP allows for the creation of shared code modules that can be used across platforms. These modules are written in Kotlin and can contain business logic, networking, data management, and more. When this shared code needs to interact with SwiftUI, a few steps are involved:

  1. Expose Kotlin Code to Swift:

    • The shared Kotlin code is compiled into a framework that can be consumed by Swift.

    • This framework is integrated into the Xcode project, making the Kotlin code accessible from Swift.

  2. Creating SwiftUI Views:

    • SwiftUI views are created using Swift. These views can call functions and use data models defined in the shared Kotlin code.

    • For example, a SwiftUI view might need to fetch data using a repository defined in the shared Kotlin code.

  3. Bridging the Code:

    • Data and functions in the shared Kotlin code are accessed using Swift's interop capabilities. The Kotlin framework exposes classes, functions, and properties that can be used directly in SwiftUI.

    • For example, a Kotlin class UserRepository can be instantiated and used in Swift to fetch user data.

  4. Updating UI with Shared Data:

    • Data fetched or processed by the Kotlin code can be used to update the SwiftUI view.

    • SwiftUI’s ObservableObject and @Published properties can help manage state and update the UI reactively when data changes.

In the shared module, create a simple repository to fetch user data.

// shared/src/commonMain/kotlin/com/example/shared/User.kt
package com.example.shared

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

// shared/src/commonMain/kotlin/com/example/shared/UserRepository.kt
package com.example.shared

class UserRepository {
    fun getUsers(): List<User> {
        return listOf(
            User(1, "John Doe", "john.doe@example.com"),
            User(2, "Jane Smith", "jane.smith@example.com")
        )
    }
}

Expose Kotlin Code to Swift

  • Ensure the Kotlin framework is generated and linked properly in the Xcode project.
// ExampleSwiftUIApp.swift
import SwiftUI
import shared

@main
struct ExampleSwiftUIApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}


class UserViewModel: ObservableObject {
    @Published var users: [User] = []

    private let repository: UserRepository

    init(repository: UserRepository = UserRepository()) {
        self.repository = repository
        loadUsers()
    }

    func loadUsers() {
        users = repository.getUsers()
    }
}

struct ContentView: View {
    @ObservedObject var viewModel = UserViewModel()

    var body: some View {
        NavigationView {
            List(viewModel.users, id: \.id) { user in
                VStack(alignment: .leading) {
                    Text(user.name)
                        .font(.headline)
                    Text(user.email)
                        .font(.subheadline)
                }
            }
            .navigationTitle("Users")
        }
    }
}

Run the Xcode project to see the SwiftUI interface displaying data fetched from the shared Kotlin code.

Limitations and Challenges

  1. Interop Complexity:

    • While Kotlin/Native allows for interop with Swift, it introduces some complexity. Data types and error handling need to be carefully managed to ensure smooth interaction.
  2. Async Handling:

    • Kotlin's coroutines and Swift's async/await mechanisms need to be bridged appropriately. This often involves using completion handlers or other bridging techniques to manage asynchronous operations.
  3. Performance Overheads:

    • Interoperability between Kotlin and Swift can introduce performance overheads, especially when dealing with large datasets or high-frequency updates.
  4. UI-Specific Logic:

    • SwiftUI is very different from traditional UIKit. The reactive nature of SwiftUI means that developers need to rethink how they handle state and side effects, especially when integrating with shared Kotlin code.
  5. Limited IDE Support:

    • The tooling for KMP is still evolving. While IntelliJ IDEA and Android Studio provide good support for Kotlin, integrating with Xcode and managing the combined project setup can be cumbersome.

Use Cases

  1. Shared Business Logic:

    • Apps where the business logic, such as authentication, networking, and data manipulation, is shared between Android and iOS, with SwiftUI handling the presentation layer.
  2. Cross-Platform Data Models:

    • Applications that utilize shared data models between platforms. For example, a social media app where the post, user, and comment models are shared, ensuring consistency across platforms.
  3. Feature Modules:

    • Developing feature-specific modules, like a chat or payment module, in Kotlin that can be integrated into both Android and iOS apps.

Conclusion

Integrating shared Kotlin code with SwiftUI in a KMP project offers a modern and efficient way to develop cross-platform applications. Despite the challenges and limitations, this approach allows for significant code reuse and consistency across platforms. By carefully managing interoperability and understanding the nuances of both Kotlin and SwiftUI, developers can leverage the best of both worlds to build robust and responsive applications.