Swift SDK for Androidを使ってiOSアプリのロジックをAndroidアプリで再利用する – every Tech Blog

はじめに

デリッシュキッチンのiOSアプリを開発している成田です。
2025年10月24日にSwift SDK for Androidのプレビュー版がリリースされました(Announcing the Swift SDK for Android)。

Swift SDK for Androidは、Swiftで書いたコードをAndroid向けにビルド・実行できるようにするためのSDKです。
今回は、そんなSwift SDK for Androidを使ったサンプルアプリの1つを実際に動かしてみながら、その応用としてiOSアプリ内の汎用的な文字列バリデーションロジックを抽出し、Androidアプリで使ってみようと思います。

サンプルプロジェクトを起動してみる

サンプルアプリを動かす前に、Swift SDK for Androidのセットアップが必要です。公式ガイドを参考に、Host Toolchain、Swift SDK、Android NDKのセットアップを行いました。詳細な手順は他の記事でも紹介されているため、ここでは省略します。

今回はサンプルアプリとして、swift-android-examplesリポジトリのhello-swift-raw-jniを動かしてみました。

Hello from Swift ❤️という文字列が画面の中央に表示される非常にシンプルなアプリになっています。

調べてみると、確かにhelloswift.swiftというSwiftファイルがあり、Swiftで書かれたコードが呼ばれているのが分かります。

import Android

@_cdecl("Java_org_example_helloswift_MainActivity_stringFromSwift")
public func MainActivity_stringFromSwift(env: UnsafeMutablePointer, clazz: jclass) -> jstring {
    let hello = ["Hello", "from", "Swift", "❤️"].joined(separator: " ")
    return hello.withCString { ptr in
        env.pointee!.pointee.NewStringUTF(env, ptr)!
    }
}

このコードでは、以下の処理を行っています。

  1. @cdecl属性: JNIの命名規則に合わせた関数名を指定します。Java{パッケージ名}{クラス名}{メソッド名}という形式で、Kotlin側のMainActivity.stringFromSwift()メソッドに対応します。
  2. JNI環境パラメータ: envはJNIのAPIを呼び出すために必要な環境へのポインタ、clazzは呼び出し元のJavaクラスです。
  3. 文字列の生成と変換: Swiftの文字列を配列から生成し、withCStringでC文字列に変換した後、NewStringUTFを使ってJavaのjstring型に変換して返します。
    このように、生のJNIを使う場合は、Swift側でJNIのAPIを直接使ってJava/Kotlin側の型と相互変換する必要があります。

JNIについて

JNI(Java Native Interface)は、Java/KotlinからC/C++を呼び出すための標準的なインターフェースです。
上記の例では、KotlinからC/C++をJNI経由で呼び出し、さらにそのC/C++からSwift関数を呼び出しています。

バリデーションロジックをAndroidで使えるようにする手順

概要

今回は、以下のような汎用的なバリデーションロジックを移行することを考えます。例えば、iOS側では以下のようにメールアドレスとパスワードの、正規表現を使ったバリデーションロジックを実装していたとします:

iOS側での使用例(Swift):

import Foundation

extension String {
    func isValidEmail() -> Bool {
        let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
        return NSPredicate(format: "SELF MATCHES %@", emailRegex).evaluate(with: self)
    }

    func isValidPassword() -> Bool {
        let passRegax = "^[a-zA-Z0-9^$*.\\[\\]{}()?\\-\"!@#%&\\/\\,><':;|_~`=+]{8,256}$"
        return NSPredicate(format: "SELF MATCHES %@", passRegax).evaluate(with: self)
    }
}

この汎用的なバリデーションロジックをAndroidでも使えるようにするため、Swift SDK for Androidを使用して移行していきます。

実装手順

実装は以下の7つのステップで進めます:

  1. Swiftライブラリの作成: Swift PackageとしてStringValidatorを実装します
  2. swift-java.configの設定: Javaラッパー生成のための設定ファイルを作成します
  3. build.gradleの設定: SwiftビルドとJavaラッパー生成の設定を追加します
  4. swiftkit-coreの公開: 必要なJavaパッケージをローカルMavenリポジトリに公開します
  5. Androidアプリの作成: validation-appを作成し、validation-libへの依存関係を追加します
  6. KotlinからSwift関数を呼び出す: 生成されたJavaラッパーを使ってKotlinから呼び出します
  7. UI実装: バリデーションをテストするためのメールアドレスとパスワードのフォームを作成します

それでは、各ステップの詳細を見ていきましょう。

1. Swiftライブラリの作成

サンプルプロジェクトには既にhello-swift-javaディレクトリ配下にhashing-lib/hashing-appという例が含まれています。これらをテンプレートとして、同じディレクトリ配下にvalidation-libディレクトリを作成し、Swift PackageとしてStringValidatorを実装してみます。

Package.swiftの設定

import CompilerPluginSupport
import PackageDescription

let package = Package(
  name: "StringValidator",
  platforms: [.macOS(.v15)],
  products: [
    .library(
      name: "StringValidator",
      type: .dynamic,
      targets: ["StringValidator"])
  ],
  dependencies: [
    .package(url: "https://github.com/swiftlang/swift-java", branch: "main"),
  ],
  targets: [
    .target(
      name: "StringValidator",
      dependencies: [
        .product(name: "SwiftJava", package: "swift-java"),
        .product(name: "CSwiftJavaJNI", package: "swift-java"),
      ],
      plugins: [
        .plugin(name: "JExtractSwiftPlugin", package: "swift-java")
      ]
    ),
  ]
)

StringValidator.swiftの実装

元のiOSコードではextension Stringとして実装されていましたが、swift-javaプラグインが自動的にJavaラッパーを生成するため、トップレベルのpublic funcとして実装します:

import Foundation
import SwiftJava

public func isValidEmail(_ email: String) -> Bool {
    let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
    return matches(pattern: emailRegex, string: email)
}

public func isValidPassword(_ password: String) -> Bool {
    guard password.count >= 8 && password.count <= 256 else {
        return false
    }
    let passRegex = "^[a-zA-Z0-9^$*.\\[\\]{}()?\\-\"!@#%&\\/\\,><':;|_~`=+]+$"
    return matches(pattern: passRegex, string: password)
}

private func matches(pattern: String, string: String) -> Bool {
    guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return false }
    let range = NSRange(location: 0, length: string.utf16.count)
    return regex.firstMatch(in: string, options: [], range: range) != nil
}

iOSコードとの違い

  • iOS側: extension StringisValidEmail()isValidPassword()として実装("test@example.com".isValidEmail()のように呼び出し)
  • Android側: トップレベルのpublic funcとして実装(isValidEmail("test@example.com")のように呼び出し)

swift-javaプラグインはトップレベルのpublic funcに対してJavaラッパーを生成するため、この形式で実装しています。機能的には同じバリデーションロジックを提供します。

2. swift-java.configの設定

swift-javaプラグインの設定ファイルを作成します。このファイルはSources/StringValidator/ディレクトリに配置します:

{
  "javaPackage": "com.example.stringvalidator",
  "mode": "jni"
}
  • javaPackage: 生成されるJavaラッパーのパッケージ名を指定
  • mode: jniモードを指定(JNI経由でSwift関数を呼び出す)

3. build.gradleの設定

hashing-libbuild.gradleを参考に、SwiftビルドとJavaラッパー生成の設定を追加します。

主な設定内容

  1. Swift SDKのパス設定: getSwiftlyPath()getSwiftSDKPath()関数でSwift SDKとSwiftlyのパスを自動検出します。
  2. 全ABI向けのビルドタスク: arm64-v8a、armeabi-v7a、x86_64の3つのABI向けにSwiftコードをビルドします。各ABIごとにbuildSwift${abi}タスクが作成されます。
  3. Swiftランタイムライブラリのコピー: Swiftランタイムライブラリ(swiftCoreFoundationなど)を自動的にコピーします。
  4. 生成されたJavaファイルのソースディレクトリへの追加: swift-javaプラグインが生成したJavaラッパーファイルを、Androidライブラリのソースセットに追加します。

build.gradleの主要な設定は以下の通りです:

plugins {
    alias(libs.plugins.android.library)
}

android {
    namespace "com.example.validationlib"
    compileSdkVersion 34
    
    defaultConfig {
        minSdkVersion 28
    }
}

dependencies {
    implementation('org.swift.swiftkit:swiftkit-core:1.0-SNAPSHOT')
}


def getSwiftlyPath() {
    
    
}

def getSwiftSDKPath() {
    
}


def abis = [
    "arm64-v8a"     : [triple: "aarch64-unknown-linux-android28", ...],
    "armeabi-v7a"   : [triple: "armv7-unknown-linux-android28", ...],
    "x86_64"        : [triple: "x86_64-unknown-linux-android28", ...]
]


def buildSwiftAll = tasks.register("buildSwiftAll") {
    inputs.file(new File(projectDir, "Package.swift"))
    inputs.dir(new File(projectDir, "Sources/StringValidator"))
    
}

abis.each { abi, info ->
    def task = tasks.register("buildSwift${abi.capitalize()}", Exec) {
        workingDir = layout.projectDirectory
        executable(getSwiftlyPath())
        args("run", "swift", "build", "+${swiftVersion}", "--swift-sdk", info.triple)
    }
    buildSwiftAll.configure { dependsOn(task) }
}


android {
    sourceSets {
        main {
            java { srcDir(buildSwiftAll) }
            jniLibs { srcDir(generatedJniLibsDir) }
        }
    }
}

preBuild.dependsOn(copyJniLibs)

4. swiftkit-coreの公開

swift-javaプロジェクトは、SwiftからJava/Kotlinへのラッパー生成に必要なJavaパッケージ(swiftkit-coreなど)をまだ公式のMavenリポジトリに公開していません。そのため、Androidプロジェクトで利用するには、ローカルMavenリポジトリに公開して参照可能にする必要があります。

SwiftやiOS開発での例に置き換えると、CocoaPodsやSwift Package ManagerでまだGitHubや公式リポジトリに公開されていないライブラリをローカルパスから利用するのと同じイメージです。Mavenリポジトリは、Java/Kotlinのライブラリを配布する仕組みで、SPMやCocoaPodsリポジトリのようなものです。

以下のコマンドをターミナルで実行します:

$ cd hello-swift-java/validation-lib
$ swift package resolve
$ ./.build/checkouts/swift-java/gradlew --project-dir .build/checkouts/swift-java :SwiftKitCore:publishToMavenLocal

5. Androidアプリの作成

validation-libと同じディレクトリ配下(hello-swift-javaディレクトリ配下)にvalidation-appディレクトリを作成し、validation-libへの依存関係を追加します。

Androidアプリのビルドと依存関係の管理には、Gradleというビルドツールを使用します。iOS開発でSwift Package Manager (SPM)のPackage.swiftやCocoaPodsのPodfileで依存関係を定義するのと同様に、Androidではbuild.gradle.ktsファイルで依存関係を定義します。また、Xcodeプロジェクトの.xcodeprojで設定を管理するのと同様に、Gradleではbuild.gradle.ktsでアプリの設定と依存関係を一括で管理します。

build.gradle.ktsの設定

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
}

android {
    namespace = "com.example.validationapp"
    compileSdk = 36
    
    defaultConfig {
        applicationId = "com.example.validationapp"
        minSdk = 28
        targetSdk = 36
    }
    
    buildFeatures {
        compose = true
    }
}

dependencies {
    implementation(project(":hello-swift-java-validation-lib"))
    
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.activity.compose)
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.ui)
    implementation(libs.androidx.material3)
    
}

6. KotlinからSwift関数を呼び出す

生成されたJavaラッパーを使って、KotlinからSwift関数を呼び出します。swift-javaプラグインが自動的に生成したStringValidatorクラスを使用します:

import com.example.stringvalidator.StringValidator

val isValid = if (email.isNotEmpty()) {
    StringValidator.isValidEmail(email)
} else null

このコードでは、swift-javaプラグインが自動生成したStringValidatorクラスの静的メソッドisValidEmail()を呼び出しています。
生成されたJavaラッパーは、Swiftのトップレベル関数をJava/Kotlinの静的メソッドとして提供するため、通常のクラスメソッドを呼び出すのと同じ感覚で、Swiftで書いた関数を利用できます。

7. UI実装

Jetpack Composeでバリデーションをテストするための入力フォームを実装します。
この辺はCursorによしなに作ってもらいました。偉大です。

MainActivity.ktの実装例(主要部分のみ)

import com.example.stringvalidator.StringValidator
import androidx.compose.runtime.*
import androidx.compose.material3.*

@Composable
fun ValidationScreen() {
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }

    Column {
        
        val isValidEmail = if (email.isNotEmpty()) {
            StringValidator.isValidEmail(email)
        } else null
        
        TextField(
            value = email,
            onValueChange = { email = it },
            isError = isValidEmail == false
        )
        
        
        val isValidPassword = if (password.isNotEmpty()) {
            StringValidator.isValidPassword(password)
        } else null
        
        TextField(
            value = password,
            onValueChange = { password = it },
            isError = isValidPassword == false
        )
    }
}

このコードでは、StringValidator.isValidEmail()StringValidator.isValidPassword()を直接呼び出すことで、Swiftで書いたバリデーションロジックを使用しています。remember { mutableStateOf(...) }で状態を管理し、TextFieldonValueChangeで値が変更されるたびにバリデーション関数が自動的に再実行されます。その結果がisErrorプロパティに反映されるため、ユーザーが入力している間、リアルタイムでバリデーション結果が表示されます。

動作確認・デモ

エミュレータでの動作確認

Android Studioでvalidation-appを実行し、エミュレータで動作確認をします。

アプリを起動すると、メールアドレスとパスワードの入力フォームが表示されます。各フィールドに入力すると、リアルタイムでバリデーションが実行され、以下のように動作します。

  • Emailフィールド: 有効なメールアドレス形式で入力すると、エラー表示が消えます。無効な形式(例:test@testなど)では、エラー状態が表示されます
  • Passwordフィールド: 8文字以上256文字以下の要件を満たす有効な文字列を入力すると、エラー表示が消えます。要件を満たさない場合は、エラー状態が表示されます

まとめ

今回は、Swift SDK for Androidを使ってiOSアプリで使っていた汎用的な文字列バリデーションロジックをAndroidアプリで再利用する手順を紹介しました。ポイントは以下の通りです。

  • Swift SDK for Androidを使うことで、既存のiOSのSwiftコードをAndroidでも活かせる
  • swift-javaプラグインを使えば、Swiftのトップレベル関数をJava/Kotlinから簡単に呼び出せる

まだプレビュー版で制約もありますが、今後はより多くのiOSアプリでのコードをAndroidで再利用できるようになると思われます。

おまけ

今日はハロウィンらしいので、おまけとしてSwiftUIだけで作った可愛いアニメーションを載せておきます(笑)。




Source link

関連記事

コメント

この記事へのコメントはありません。