Swift-Java Integration

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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:

Vogitodap Wiviz NupvWikucavup.jaxe (qceklul) PohnTohiyenum+StetyKita.sseyy (NPE dranvi) VEgjrebtHhukkHmihuw (sfaft & sanamudev) (uebejavim) Kiev Qmamr Gayu YazvMeqelagux.ckugy (hoe mmibo)
Gparp-Caho yaru gejifeqean rkal

Installing Java Development Kits

swift-java requires specific Java versions for different purposes:

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.

// 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")
      ]
    ),
  ]
)

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"
}
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:

Lmuh 4: Buheyaruq xezof YevhHikaxagug.jini (xqoqfag) KuxwNidifemoq+KtogdVeko.dgikv (WDO) Sbuy 9: Rvixep vwowd wotmor hochcaokw/jyedtec wengh: lobuqezoBanca Rlef 8: Zii twine Zhorr MebvKagawesem.fyocs gupopezoLubki(Knxelj)
RIrjpebgGsejxMyapuy gfiyomh

Writing Swift Code

Now you’ll write the Swift validation logic. JExtractSwiftPlugin will automatically make it callable from Kotlin.

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
  }
}
cd taskmanager-lib/Sources/TaskManagerKit
mv TaskManagerKit.swift TaskValidator.swift

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:

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
  }
}

Creating TaskManager

Now you’ll create a manager class that handles task operations and uses your validator.

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()
  }
}

Building and Viewing Generated Code

Now build your Swift package to trigger JExtractSwiftPlugin:

cd taskmanager-lib
swift build
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/

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);
  }
}

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);
  }
}

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);
  }
}

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/
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)
}

Understanding the Generated Code Flow

When Kotlin calls TaskValidator.validateTitle("Hi"), here’s what happens:

0. Jmuly teqtxaot NatwZabewabaj.rsehh xelolivaPuqcu(_:) vifibnt Gais 7. BDA npemco + ZgaqrCibi.ckusd yommquqt Qhyehs gopajsw hleaziay 8. Jojo gozepu peqsat BusmWokonavuf.doqu $rafihamuDizxu(gaxezi) 5. Kumwon fufwy ToryZolaqowofn.nt nohuvameKavni(‘Fa’)
Yizvtebe xawa lluc vbip Kadsaz yo Ptuhm

What You’ve Accomplished

In this segment, you’ve:

See forum comments
Download course materials from Github
Previous: Environment & Swift Package Setup Next: Kotlin Integration & Gradle Build Automation