Swift-Java Integration
In traditional JNI development, you’d write complex bridge code to connect Java and native libraries. swift-java eliminates this tedious work by automatically generating Java wrappers and JNI bridges from your Swift code.
What is swift-java?
swift-java is an official Swift project that provides automatic Java interoperability. It consists of:
- SwiftJava: Runtime support library for Swift-Java communication
- JExtractSwiftPlugin: Build plugin that scans Swift code and generates Java classes
- SwiftKitCore: Java runtime library that manages JNI connections
Here’s what happens with swift-java:
You write normal Swift code, and the plugin handles all the JNI complexity!
Note: This approach is what production Swift-Android apps use. It eliminates the need to write and maintain manual JNI code, which is error-prone and time-consuming.
Installing Java Development Kits
swift-java requires specific Java versions for different purposes:
- JDK 25: One-time setup to publish swift-java’s runtime library (you’ll do this in the next segment)
- JDK 21: All actual Swift compilation and Android builds
Important: You need JDK 25 only for one publishing step in the next segment. For now, you’ll use JDK 21 for all your Swift compilation. JDK 25 isn’t used for day-to-day development.
Install sdkman
sdkman is a Java version manager that makes it easy to install and switch between JDK versions.
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
Install Both JDK Versions
# Install JDK 21 for Swift builds
sdk install java 21.0.5-tem
# Install JDK 25 for swift-java publishing (needed in next segment)
sdk install java 25.0.1-amzn
# Use JDK 21 by default
sdk use java 21.0.5-tem
# Verify
java -version
# Should show: openjdk version "21.0.5"
Configuring Package.swift for swift-java
Now you’ll update your Package.swift to integrate swift-java. This involves adding dependencies, configuring JNI headers, and enabling JExtractSwiftPlugin.
Open taskmanager-lib/Package.swift and replace its contents:
// swift-tools-version: 6.2
import CompilerPluginSupport
import PackageDescription
import class Foundation.FileManager
import class Foundation.ProcessInfo
// 1: Helper to find Java installation
func findJavaHome() -> String {
if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] {
return home
}
// Fallback: read from ~/.java_home file
let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home"
if let home = try? String(contentsOfFile: path, encoding: .utf8) {
if let lastChar = home.last, lastChar.isNewline {
return String(home.dropLast())
}
return home
}
fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.")
}
let javaHome = findJavaHome()
// 2: Construct JNI include paths
let javaIncludePath = "\(javaHome)/include"
let javaPlatformIncludePath = "\(javaIncludePath)/darwin"
let package = Package(
name: "TaskManagerKit",
platforms: [.macOS(.v15)],
products: [
// 3: Dynamic library for Android
.library(
name: "TaskManagerKit",
type: .dynamic,
targets: ["TaskManagerKit"]
)
],
dependencies: [
// 4: swift-java dependency
.package(url: "https://github.com/swiftlang/swift-java", branch: "main"),
],
targets: [
.target(
name: "TaskManagerKit",
dependencies: [
// 5: swift-java products
.product(name: "SwiftJava", package: "swift-java"),
],
resources: [
// 6: JExtract configuration
.copy("swift-java.config")
],
swiftSettings: [
.swiftLanguageMode(.v5),
// 7: JNI header paths
.unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"])
],
plugins: [
// 8: The magic plugin!
.plugin(name: "JExtractSwiftPlugin", package: "swift-java")
]
),
]
)
What each section does:
-
findJavaHome(): Locates your Java installation from the
JAVA_HOMEenvironment variable. -
JNI include paths: Constructs paths to JNI headers (
jni.h,jni_md.h) needed for JNI compilation. -
.library(type: .dynamic): Critical - creates a shared library (
.so) that Android loads at runtime. - swift-java dependency: Adds the swift-java package from GitHub.
- swift-java products: Links SwiftJava runtime support into your package.
- swift-java.config: Resources file that configures how JExtractSwiftPlugin generates code.
- unsafeFlags: Tells the Swift compiler where to find JNI headers.
- JExtractSwiftPlugin: The build plugin that scans your Swift code and generates Java bindings.
Configuring JExtractSwiftPlugin
JExtractSwiftPlugin needs to know what Java package to use for generated classes. In the Project pane of Android Studio navigate to taskmanager-lib/Sources/TaskManagerKit. Now create swift-java.config with this content:
{
"javaPackage": "com.kodeco.android.taskmanagerkit",
"mode": "jni"
}
Configuration explained:
- javaPackage: The Java package name for generated classes (matches your Android package structure)
-
mode: Use
"jni"mode for JNI-based interop (vs other modes like GraalVM)
Your directory structure should now look like:
taskmanager-lib/
├── Package.swift ← Updated with swift-java
└── Sources/
└── TaskManagerKit/
├── TaskManagerKit.swift ← Will replace with TaskValidator
└── swift-java.config ← NEW: JExtract configuration
How JExtractSwiftPlugin Works
When you build your Swift code, here’s the complete flow:
The plugin:
- Scans all Swift files in your target
- Finds public functions and classes that should be exposed to Java
-
Generates Java wrapper classes with
nativemethod declarations - Generates Swift JNI bridge code that handles type conversion
-
Outputs files to
.build/plugins/outputs/...where Gradle picks them up
You never write JNI code manually - it’s all automated!
Writing Swift Code
Now you’ll write the Swift validation logic. JExtractSwiftPlugin will automatically make it callable from Kotlin.
Replace the contents of taskmanager-lib/Sources/TaskManagerKit/TaskManagerKit.swift with:
import Foundation
// 1: TaskValidator class
public class TaskValidator {
// 2: Validation constants
private static let minTitleLength = 3
private static let maxTitleLength = 50
private static let minDescriptionLength = 10
private static let maxDescriptionLength = 200
// 3: Title validation
public static func validateTitle(_ title: String) -> Bool {
let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.count >= minTitleLength && trimmed.count <= maxTitleLength
}
// 4: Description validation
public static func validateDescription(_ description: String) -> Bool {
let trimmed = description.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.count >= minDescriptionLength && trimmed.count <= maxDescriptionLength
}
}
Save the file and rename it to better match its purpose:
cd taskmanager-lib/Sources/TaskManagerKit
mv TaskManagerKit.swift TaskValidator.swift
What this code does:
- TaskValidator class: A simple class with static methods (no instances needed)
- Validation constants: Minimum and maximum lengths for titles and descriptions
- validateTitle(): Trims whitespace and checks if length is between 3-50 characters
- validateDescription(): Trims whitespace and checks if length is between 10-200 characters
Notice what’s missing:
- No
@_cdeclexports: this Swift attribute would normally be required to expose functions to JNI, but swift-java generates all the export code automatically - No JNI function names
- No type marshaling code
- No native keyword
Just clean, normal Swift code! JExtractSwiftPlugin handles the rest.
Creating the Task Data Model
Now you’ll create the Task data model in Swift. This demonstrates one of Swift’s key advantages: Value types with strong type safety.
Why Create Models in Swift?
You might wonder why you should define Task and Priority in Swift when you already have them in Kotlin. Here’s why:
- Value semantics: Swift structs provide automatic copying and immutability benefits.
- Type safety: Swift’s enum associated values prevent invalid states.
- Codable: Built-in serialization without boilerplate.
- Shared validation: Task creation logic lives with validation in Swift.
- Single source of truth: Business models defined once, generated for Kotlin.
With swift-java, you write the model once in Swift, and JExtractSwiftPlugin generates the Kotlin-compatible classes automatically.
Create Task.swift
Create a new file taskmanager-lib/Sources/TaskManagerKit/Task.swift:
import Foundation
// 1
public enum Priority: String, Codable {
case low = "Low"
case medium = "Medium"
case high = "High"
}
// 2
public struct Task: Codable {
// 3
public let id: String
public let title: String
public let description: String
public let priority: Priority
public var isCompleted: Bool
// 4
public init(id: String, title: String, description: String, priority: Priority, isCompleted: Bool = false) {
self.id = id
self.title = title
self.description = description
self.priority = priority
self.isCompleted = isCompleted
}
}
What this code does:
-
Priority enum: Defines task priority levels with string raw values. The
Codableprotocol enables JSON serialization. - Task struct: A value type that represents a task. Structs provide automatic copying and thread safety.
-
Properties: All required task data. Note
isCompletedisvar(mutable) while others arelet(immutable). -
Public initializer: Required for creating Task instances from Kotlin. Default value for
isCompletedmakes it optional.
Key Swift concepts:
- Struct vs Class: Structs are value types (copied) while classes are reference types (shared). For data models, structs are safer.
- Codable: Protocol that enables automatic encoding/decoding to JSON, Plist, etc.
- Raw values: The enum’s string values make it easy to display and serialize.
JExtractSwiftPlugin will generate Java classes for both Priority and Task that Kotlin can use directly!
Creating TaskManager
Now you’ll create a manager class that handles task operations and uses your validator.
Create taskmanager-lib/Sources/TaskManagerKit/TaskManager.swift:
import Foundation
// 1
public class TaskManager: @unchecked Sendable {
// 2
private var tasks: [Task] = []
// 3
public static let shared = TaskManager()
// 4
private init() {}
// 5
@discardableResult
public func addTask(_ task: Task) -> Bool {
// 6
guard TaskValidator.validateTitle(task.title),
TaskValidator.validateDescription(task.description) else {
return false
}
// 7
tasks.append(task)
return true
}
// 8
public func getTasks() -> [Task] {
return tasks
}
// 9
public func getTaskCount() -> Int64 {
return Int64(tasks.count)
}
// 10
public func clearTasks() {
tasks.removeAll()
}
}
What this code does:
-
TaskManager class: A reference type marked
@unchecked Sendableto indicate it can be safely shared across threads (you’ll handle thread safety in Kotlin). - Private storage: Internal array of tasks, not directly accessible from outside.
-
Singleton instance: The
sharedproperty provides a single instance accessible from Kotlin. -
Private initializer: Prevents creating multiple instances - only
sharedcan be used. -
addTask method: Takes a Task, validates it, and adds to the array. Returns
Boolto indicate success. -
Validation guard: Uses TaskValidator to check the task before adding. If validation fails, returns
false. -
Append task: If validation passes, adds the task to the array and returns
true. - getTasks: Returns all tasks as an array.
-
getTaskCount: Returns count as
Int64(better for JNI interop thanInt). - clearTasks: Removes all tasks from the array.
Key concepts:
-
Singleton pattern: One shared instance accessed via
TaskManager.shared, and a private initializer. - Guard statements: Early exit pattern for validation.
-
@discardableResult: Allows calling
addTaskwithout using the return value. -
Int64: Explicitly sized integer type that maps cleanly to Java
long.
This demonstrates how business logic (validation + storage) can live entirely in Swift!
Building and Viewing Generated Code
Now build your Swift package to trigger JExtractSwiftPlugin:
cd taskmanager-lib
swift build
During the build, you’ll see output showing the plugin running:
Building for debugging...
...
Build complete!
Check Generated Java Code
After the build, JExtractSwiftPlugin generates Java wrapper classes in this directory:
.build/plugins/outputs/taskmanager-lib/TaskManagerKit/destination/JExtractSwiftPlugin/src/generated/java/com/kodeco/android/taskmanagerkit/
In Android Studio’s Project pane, navigate to this folder. You should see four generated Java files:
- TaskValidator.java - Wrapper for TaskValidator class.
- Task.java - Wrapper for Task struct.
- Priority.java - Wrapper for Priority enum (enums get their own Java file!).
- TaskManager.java - Wrapper for TaskManager class.
Let’s examine each generated file. The actual generated files are quite extensive with full JNI implementation details - the code snippets below show the key concepts to understand how Swift code maps to Java.
TaskValidator.java
Open TaskValidator.java in Android Studio. The generated file contains JNI wrapper code - here are the key parts:
package com.kodeco.android.taskmanagerkit;
public final class TaskValidator {
static final String LIB_NAME = "TaskManagerKit";
public static boolean validateTitle(String title) {
return TaskValidator.$validateTitle(title);
}
private static native boolean $validateTitle(String title);
public static boolean validateDescription(String description) {
return TaskValidator.$validateDescription(description);
}
private static native boolean $validateDescription(String description);
static {
System.loadLibrary(LIB_NAME);
}
}
Key points:
- public static methods: Match your Swift functions exactly.
-
native methods: Marked with
nativekeyword - these call into Swift via JNI. - System.loadLibrary(): Loads your Swift library (.so file) at runtime.
Task.java and Priority.java
Open Task.java in Android Studio. The file is quite long, but here are the key parts showing how Swift structs map to Java classes:
package com.kodeco.android.taskmanagerkit;
import org.swift.swiftkit.SwiftArena;
public final class Task {
static final String LIB_NAME = "TaskManagerKit";
// Native methods for property access
private static native String $getId(long swiftArenaPtr, long objectPtr);
private static native String $getTitle(long swiftArenaPtr, long objectPtr);
private static native String $getDescription(long swiftArenaPtr, long objectPtr);
private static native long $getPriority(long swiftArenaPtr, long objectPtr);
private static native boolean $getIsCompleted(long swiftArenaPtr, long objectPtr);
// Constructor
public Task(SwiftArena arena, String id, String title, String description,
Priority priority, boolean isCompleted) {
// ... constructor implementation
}
// Getters
public String getId(SwiftArena arena) { /* ... */ }
public String getTitle(SwiftArena arena) { /* ... */ }
public String getDescription(SwiftArena arena) { /* ... */ }
public Priority getPriority(SwiftArena arena) { /* ... */ }
public boolean getIsCompleted(SwiftArena arena) { /* ... */ }
static {
System.loadLibrary(LIB_NAME);
}
}
Notice:
- SwiftArena parameter: Swift reference types require arena-based memory management from Java.
- Native getters: Each property access calls into Swift.
- Constructor: Creates Swift Task instances from Java.
Priority.java is a simple enum wrapper - open it to see how Swift enums map to Java enums.
TaskManager.java
Open TaskManager.java in Android Studio. Here’s a simplified view of the key methods:
package com.kodeco.android.taskmanagerkit;
import org.swift.swiftkit.SwiftArena;
public final class TaskManager {
static final String LIB_NAME = "TaskManagerKit";
// Singleton access
public static TaskManager getShared(SwiftArena arena) {
// Returns the Swift singleton instance
}
private static native long $getShared(long swiftArenaPtr);
// Methods
public boolean addTask(SwiftArena arena, Task task) { /* ... */ }
public Task[] getTasks(SwiftArena arena) { /* ... */ }
public long getTaskCount(SwiftArena arena) { /* ... */ }
public void clearTasks(SwiftArena arena) { /* ... */ }
static {
System.loadLibrary(LIB_NAME);
}
}
Key aspects:
- getShared(): Accesses the Swift singleton from Java.
- Arena-managed: Reference type requires SwiftArena for memory management.
-
Array conversion:
getTasks()automatically converts Swift arrays to Java arrays.
All three classes are clean, idiomatic Java that Kotlin can use directly!
Check Generated Swift JNI Bridge
JExtractSwiftPlugin also generated Swift code that handles the JNI calls. These are located in:
.build/plugins/outputs/taskmanager-lib/TaskManagerKit/destination/JExtractSwiftPlugin/Sources/
You should see three Swift JNI bridge files:
TaskValidator+SwiftJava.swiftTask+SwiftJava.swiftTaskManager+SwiftJava.swift
If you open TaskValidator+SwiftJava.swift, you’ll see complex JNI code like:
import SwiftJava
import CSwiftJavaJNI
import SwiftJavaRuntimeSupport
@_cdecl("Java_com_kodeco_android_taskmanagerkit_TaskValidator__00024validateTitle__Ljava_lang_String_2")
public func Java_com_kodeco_android_taskmanagerkit_TaskValidator__00024validateTitle__Ljava_lang_String_2(
environment: UnsafeMutablePointer<JNIEnv?>!,
thisClass: jclass,
title: jstring?
) -> jboolean {
return TaskValidator.validateTitle(
String(fromJNI: title, in: environment)
).getJNIValue(in: environment)
}
You never have to write or maintain this code! JExtractSwiftPlugin regenerates it automatically every time you build. Each of your three Swift files gets its own JNI bridge with all the type marshaling handled automatically.
Understanding the Generated Code Flow
When Kotlin calls TaskValidator.validateTitle("Hi"), here’s what happens:
This same flow works for all three generated classes - TaskValidator, Task, and TaskManager. All the type conversion, JNI setup, and marshaling happens automatically in the generated code.
What gets generated for each Swift file:
- TaskValidator.swift → TaskValidator.java + TaskValidator+SwiftJava.swift
- Task.swift → Task.java + Priority.java + Task+SwiftJava.swift
- TaskManager.swift → TaskManager.java + TaskManager+SwiftJava.swift
What You’ve Accomplished
In this segment, you’ve:
- Installed JDK 25 and JDK 21 with sdkman.
- Updated Package.swift with swift-java dependencies.
- Configured JExtractSwiftPlugin with swift-java.config.
- Written three Swift files: TaskValidator, Task, and TaskManager.
- Built the package and viewed auto-generated Java and JNI bridge code.
- Understood how JExtractSwiftPlugin generates Java wrappers for all your Swift code.
- Learned how value types (struct) and reference types (class) generate different Java APIs.
Your Swift code now auto-generates complete Java bindings! In the next segment, you’ll publish the SwiftKitCore runtime, connect everything to your Android app through Gradle, and build the complete application.