4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Swift SDK for Androidを完全に理解する

Posted at

blog-hero-dark@2x (1).png

これは何?

Swift SDK for Androidについて網羅的に理解できる記事です。大きく分けて次のことを理解できるようになります。

  • Swift SDK for Androidとは何か?
  • AndroidアプリからSwiftを呼び出す仕組み
  • サンプルアプリを用いた細かい挙動

注意事項

  • Android Workgroupに問い合わせたものではないので100%正しいとは限らないです。間違ってたらコメントくださいmm
  • Swift SDK for Androidは日々更新されているので、この記事の内容が古い情報になっている可能性があります。
  • 生成AIによる文章の生成は一切していません。1文字ずつ真心込めて入力しています。なので分かりにくいところもあるかと思いますが、頑張って理解するか、超頑張って理解してください。

Swift SDK for Androidとは

ざっくりいうと、SwiftをAndroidで動かすためのツール。
AndroidをSwiftの公式サポートにすることを目的としているAndroid Workgroupにより、2025年10月にNightly Preview版として発表された。

Nightly Preview版: まだまだ初版で安定してるわけでもないので毎晩のように更新していくぜ!みたいな状態。

Skipとは違うの?

SkipではビジネスロジックとUIの全てをSwiftで記述できるのを思想として掲げている。
それに対してSwift SDK for AndroidではビジネスロジックのみをSwiftで定義し、共有することを目指している。
これは今後の方向性を示すVision Documentに記載されている。

  • Full SwiftUI/UIKit Port: A direct, full port of existing UI frameworks to Android is not a goal. While projects like Skip.tools demonstrate bridging SwiftUI to Jetpack Compose, the official Swift on Android effort will focus on language and core library support, not building our own cross-platform UI frameworks.

このVision Documentは執筆している現段階ではまだ策定中っぽい。

AndroidアプリからSwiftを呼び出す全体像

イメージをつけるため先に全体像を示しておく。初見だと理解できないかもだが、最後まで見てから見返すと理解できると思う。

※ ここでは推奨されているswift-javaを用いた方法を前提としています。

summary.png

左の図ではビルド時の挙動をまとめている

  1. swift-javaにより以下を生成
    • Java/Kotlinから.soを呼び出すJava Wrapper
    • Swiftの処理を呼び出すためのJNI Binding
  2. Swift SDK for Androidを用いてSwiftとJNI Bindingから.soをビルド

右の図ではAndroidアプリからSwiftの呼び出しの流れをまとめている。

Swift SDK for Androidができること

Swiftのモジュールを.soとしてビルドすることができる。
build.jpg
通常であれば、SwiftがCompileされ、.oになった後、Linkすることで.appや.dylib, 実行可能バイナリなどが生成される。
これをSwift SDK for Androidを用いることで、Android向けの動的ライブラリである.soにビルドできる。

.soとは

  • Linuxなどで使用される共有ライブラリ(Shared Object)
  • iOSの動的ライブラリは.dylibだが、それのAndroid版が.so
  • AndroidでCなどのNativeコードを呼び出す場合によく使われる
    • Flutter → libflutter.soにflutterの核となる処理が定義
    • Skip → libHogeHoge.soのようにSwiftで書いたコードがビルドされて入ってる

hashing.jpg

イメージとしては上記のSwiftのhash関数もビルドすることでlibSwiftHashing.soとしてビルドされるイメージ。

環境構築

概要を理解したところで環境構築を行う。
必要なツールは次の通り。

  • SwiftのToolchain
    • Swiftをビルド、実行するために使用するSwiftコマンドと関連ツール
  • Swift SDK for Android
    • Android target向けにSwiftコードを生成および実行するために必要なライブラリ、ヘッダー、その他のリソース群
  • Android NDK
    • Native Development Kit
    • clangやldなどのToolchainをクロスコンパイルとリンクに使用する
  • Android Studio

Android Studioはなるべく最新のものを使うこと。
次に各ツールのインストールについて解説する。

Swift Toolchainのインストール

Xcodeをインストール済みで既にToolchainが入っていることも多いが、Swift SDK for Androidを使用するにはSwiftのToolchainとSwift SDK for Androidのversionが一致している必要がある。

swiftlyを使って個別にインストールするのが最も楽なので推奨されている。

$ swiftly install main-snapshot-2025-10-16
$ swiftly use main-snapshot-2025-10-16
$ swiftly run swift --version

swiftlyのインストール

(未インストールの方向け)
swiftlyはSwiftのToolchainを管理・インストールするためのツール。

  1. 公式からinstall
  2. $ swiftly init
  3. $ swiftly --versionでversionが表示されたらOK

Swift SDK for Androidのインストール

$ swift sdk install https://download.swift.org/development/android-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-16-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-16-a_android-0.1.artifactbundle.tar.gz --checksum 451844c232cf1fa02c52431084ed3dc27a42d103635c6fa71bae8d66adba2500

次のコマンドでSwift SDK for Androidが含まれていることを確認できる

$ swiftly run swift sdk list

Android NDKのインストールと設定

Swift SDK for AndroidはAndroid NDK version 27dに依存している。これはAndroidアーキテクチャへのクロスコンパイルに必要なheaderとtoolを提供するため。

$ mkdir ~/android-ndk
$ cd ~/android-ndk
$ curl -fSLO https://dl.google.com/android/repository/android-ndk-r27d-$(uname -s).zip
$ unzip -q android-ndk-r27d-*.zip
$ export ANDROID_NDK_HOME=$PWD/android-ndk-r27d

次に、Swift SDK for AndroidにNDKをリンクするため、setup-android-sdk.shを実行する

$ cd ~/Library/org.swift.swiftpm || cd ~/.swiftpm
$ ./swift-sdks/swift-DEVELOPMENT-SNAPSHOT-2025-10-16-a-android-0.1.artifactbundle/swift-android/scripts/setup-android-sdk.sh

以上でAndroid上でSwiftを動作させる最低限の環境は整った。

Androidアプリ上でJava/KotlinからSwiftを呼び出すには更にセットアップが必要だが、それについては後述する。

Android上でSwiftのHello Worldを実行する

適当なフォルダで実行可能なSwift Packageを作成する

$ mkdir hello
$ cd hello
$ swiftly run swift package init --type executable

試しにmacOS向けにビルドしてhello worldできることを確認する。

$ swiftly run swift build
$ .build/debug/hello

続けてAndroid(aarch64)向けにビルドする。

$ swiftly run swift build --swift-sdk aarch64-unknown-linux-android28 --static-swift-stdlib
$ file .build/aarch64-unknown-linux-android28/debug/hello
 
 .build/aarch64-unknown-linux-android28/debug/hello: ELF 64-bit LSB pie executable ...

x86_64はIntel/AMD系のCPUアーキテクチャ。aarch64はARMのCPUアーキテクチャ。Androidスマホのほとんどがaarch64。
もし実行するAndroidがx86_64の場合はaarch64の部分をx86_64に置き換えて実行すればOK

Android上で実行

USBデバッグを有効にしたAndroidの実機をMacに接続するか、Emulatorを起動する。
そしてaarch64向けにビルドした実行可能ファイルと、Android NDKに含まれているlibc++_shared.soを端末にコピーし、実行する。

$ adb push .build/aarch64-unknown-linux-android28/debug/hello /data/local/tmp
$ adb push $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/*/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so /data/local/tmp/
$ adb shell /data/local/tmp/hello

Hello, world!

ここでわかるように、Swift SDK for Androidは.soの他に、Android上で実行できる「ELFの実行可能バイナリ」も生成できる。

※ libc++_shared.so:
Android NDKが提供するC++標準ライブラリ。SwiftのランタイムはC++に依存しているため、実行可能ファイル以外にもlibc++_shared.soも端末にコピーする必要がある。

Androidアプリ上からSwiftを呼び出すサンプルを見る

swift-android-examplesには複数サンプルがあり、それぞれアプローチが分かれている。
その中でもhello-swift-javaサンプルが推奨アプローチ。

このサンプルでは名前からもわかる通り、swift-javaを使ってJava/KotlinからシームレスにSwiftを呼び出す方法を示している。

swift-javaは、SwiftからJavaを呼び出したり、JavaからSwiftを呼び出すのを容易に実現するためのツール。

このサンプルでは、Jetpack ComposeのTextFieldに文字列を入力してButtonを押すことで、Swiftで定義されたHash関数を呼び出し、Hash化した文字列を表示している。

構成

このサンプルでは、次の構成に分かれている

  • hashing-app
    • Androidアプリ側の実装
    • JetPack Composeを使ってSwiftを呼び出す
  • hashing-lib
    • SwiftのHash関数を定義したSwift Package

セットアップ

前述したSwift SDK for Androidの環境構築を終えている前提。
2つ設定を行う

  • JDK 25のインストール
  • Java Packageをローカルにpublish

JDK 25のインストール

この後行うローカルへの公開で必要になるためJDK 25をインストールする。Javaのインストールや管理を簡単に行う方法としてsdkmanを使用する。

sdkmanのインストール

$ curl -s "https://get.sdkman.io" | bash

sdkコマンドを利用するためにterminalを再起動し、次のコマンドでJDK 25をインストールして利用する設定にする

$ sdk install java 25.0.1-amzn --use 
$ export JAVA_HOME="${HOME}/.sdkman/candidates/java/current"

Java Packageをローカルにpublish

Javaでは標準的に使われるライブラリを管理するリポジトリとしてMaven Centralというのが存在する。
swift-javaはまだ開発途中であるため、使用しているSwiftKit系のJava Packageをローカルにpublishして利用する必要がある。

hashing-libディレクトリに移動

$ cd hashing-lib

Swift Packageの依存関係の解決

$ swift package resolve

Java PackageをローカルのMavenリポジトリへpublish

$ ./.build/checkouts/swift-java/gradlew --project-dir .build/checkouts/swift-java :SwiftKitCore:publishToMavenLocal

...
BUILD SUCCESSFUL in 1m 1s

実行する

  1. swift-android-examplesフォルダ自体をAndroid Studioで開く

  2. hashing-appをGradle Targetで選択する
    スクリーンショット 2025-12-04 8.35.58.png

  3. PreferenceからGradle JDKを25系にする
    スクリーンショット 2025-12-04 8.34.26.png

  4. Emulator or 実機でRunする

多少時間がかかるがビルドできるはず。ここで問題がある場合はAndroid Studioが古いとか、間違ったフォルダを開いてる可能性がある。

Swift Packageを見る

hashing-libのPackage.swiftを見るとswift-javaに依存し、pluginsとしてJExtractを使用していることがわかる。

dependencies: [
  .package(url: "https://github.com/swiftlang/swift-java", branch: "main"),
],
targets: [
  .target(
    name: "SwiftHashing",
    dependencies: [
      .product(name: "SwiftJava", package: "swift-java"),
      .product(name: "CSwiftJavaJNI", package: "swift-java"),
      .product(name: "SwiftJavaRuntimeSupport", package: "swift-java"),
    ],
    plugins: [
      .plugin(name: "JExtractSwiftPlugin", package: "swift-java")
    ]
  ),
]
// ※必要箇所だけ抜粋したものです

JExtractはJavaからSwiftを呼び出す時に必要なコードを自動生成してくれるツール。
詳細は省くが、jextractにはffmモード、jniモードがあり、Androidではjniモードを利用する。

これを用いることでビルド時にJava Wrapperや、JNI Bindingを生成してくれる。

Jetpack ComposeからSwiftのHash関数を呼ぶ流れ

ここでJetpack ComposeのHashボタンを押してからSwiftのHash関数を呼ぶ流れを簡潔にまとめる。
それぞれの役割の詳細は後述する。

nagare.png

  1. Hashボタンタップ
    • Java Wrapperを呼びだす
  2. Java Wrapper
    • .so内のJNIバインディングを呼び出す
  3. JNI Binding
    • hash関数を.so内でもわかるようにシンボルを定義
    • hash関数の呼び出し
  4. Swiftのhash関数が呼ばれる

図からわかるように、JNI Bindingはビルド時に生成された後、Swiftのhash関数(モジュール)と共に.soへとビルドされる。
そしてSwiftのhash関数やJNI BindingはJavaから直接コードとして読まれるというより、.soのバイナリから処理が呼ばれるような形になっている。

Swiftのコードと.soを見る

Hash化するSwiftのコードはhashing-lib/Sources/SwiftHashing/SwiftHashing.swiftにある。

import Crypto
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

public func hash(_ input: String) -> String {
    SHA256.hash(data: Data(input.utf8)).description
}

このSwiftコードがビルドされた後の.soを見てみる。(前述した通りこの.soにはJNI Bindingも含まれる)
.soは.apk配下に存在し、.apkはhashing-app/build/intermediates/apk/debug/hello-swift-java-hashing-app-debug.apkにある。

.apkはzipなので解凍すると

$ unzip hello-swift-java-hashing-app-debug.apk

hashing-app/build/intermediates/apk/debug/lib配下に.soファイルが大量にあることが確認でき、libSwiftHashing.soもあることがわかる。

lib/
└── arm64-v8a
    ├── lib_FoundationICU.so
    ├── libandroidx.graphics.path.so
    ├── libBlocksRuntime.so
    ├── libc++_shared.so
    ├── libdispatch.so
    ├── libFoundation.so
    ├── libFoundationEssentials.so
    ├── libFoundationInternationalization.so
    ├── libswift_Builtin_float.so
    ├── libswift_Concurrency.so
    ├── libswift_math.so
    ├── libswift_RegexParser.so
    ├── libswift_StringProcessing.so
    ├── libswiftAndroid.so
    ├── libswiftCore.so
    ├── libswiftDispatch.so
    ├── libSwiftHashing.so ←←←←←←←←←←←←←←←←←←←← これ
    ├── libswiftSwiftOnoneSupport.so
    └── libswiftSynchronization.so

最初にAndroid上でSwiftのHello Worldを実行した時と同じようにlibc++_shared.soも確認できる。

JNI BindingからSwiftの呼び出し

JNI Bindingは、Java/KotlinからNativeコード(Swift)を呼ぶための「繋ぎめ」みたいなもの

  • 後述するJava Wrapperのnativeメソッドと.so内のCシンボルを対応づける
  • 実際にSwiftの定義を呼び出す
  • .swiftファイル

今回のHash関数を元に生成されたJNIは以下。

// SwiftHashingModule+SwiftJava.swift

// Generated by swift-java

import SwiftJava
import CSwiftJavaJNI
import SwiftJavaRuntimeSupport

@_cdecl("Java_com_example_swifthashing_SwiftHashing__00024hash__Ljava_lang_String_2")
func Java_com_example_swifthashing_SwiftHashing__00024hash__Ljava_lang_String_2(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, input: jstring?) -> jstring? {
  return SwiftHashing.hash(String(fromJNI: input, in: environment)).getJNIValue(in: environment)
}
  • @ _cdecl
    • このSwift関数を、指定したCの関数のシンボルとして.soにexportする
    • 名前はJNIのルールに従う必要がある: Java_<パッケージ>_<クラス名>_<メソッド名>__<シグネチャ>
  • returnのところでSwiftHashing.hash()としてSwiftHashing.swiftのhash関数を呼び出している

実際に.so内を見るとJava_com_example_swifthashing_SwiftHashing__00024hash__Ljava_lang_String_2が存在していることがわかる
スクリーンショット 2025-11-24 18.54.21.png

Java WrapperからJNI Bindingの呼び出し

Java Wrapperは、JNI Bindingを呼び出すためのWrapperクラス

  • Javaから.soを呼び出す
  • .so内に定義されたJNI Bindingのシンボルを探して呼び出す
  • .javaファイル
// SwiftHashing.java

// Generated by jextract-swift
// Swift module: SwiftHashing

package com.example.swifthashing;

import org.swift.swiftkit.core.*;
import org.swift.swiftkit.core.util.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import org.swift.swiftkit.core.annotations.*;

public final class SwiftHashing {
  static final String LIB_NAME = "SwiftHashing"; // libSwiftHashing.soを指定
  
  static {
    System.loadLibrary(LIB_NAME); // .soを呼び出す
  }

  // Jetpack Composeなどから呼び出すpublicなAPI
  public static java.lang.String hash(java.lang.String input) {
    return SwiftHashing.$hash(input); // nativeメソッドの呼び出し
  } 

  // nativeメソッド
  private static native java.lang.String $hash(java.lang.String input);
  
}

次のような仕組み

  1. Jetpack Composeなどからpublicなhash関数を呼ぶ
  2. publicなhash関数内で.$hashを指定することにより、privateのnativeのhash関数を呼ぶ
  3. nativeなのでNative側のコードを.soから探して呼び出す。つまりJNI Bindingを呼びだす

nativeを定義することで、命名ルール的にJava_com_example_swifthashing_SwiftHashing__00024hash__Ljava_lang_String_2を.so内から探してくるようになる。
その結果、JNI Bindingが呼ばれる。

ButtonからJava Wrapperの呼び出し

MainActivity.ktではJava Wrapperを呼び出している。

        Button(
            colors = ButtonDefaults.buttonColors(
                containerColor = Color(0xFFF05138),
                contentColor = Color.White
            ),
            onClick = {
                // This calls the Swift method `hash` from SwiftHashing.swift
                hashResult.value = SwiftHashing.hash(input.value)
            }
        ) {
            Text("Hash")
        }

という一連の流れでAndroidアプリからSwiftを呼び出している。
まとめると以下のような形。

result.png

build.gradleを見る

ビルド時にbuild.gradleが呼ばれるので中身をみると、実行時に--swift-sdkを指定しているのでAndroid向けにビルドしていることがわかる。

// Create a build task for each ABI
abis.each { abi, info ->
    def task = tasks.register("buildSwift${abi.capitalize()}", Exec) {
        group = "build"
        description = "Builds the Swift code for the ${abi} ABI."

        doFirst {
            println("Building Swift for ${abi} (${info.triple})...")
        }

        outputs.dir(layout.projectDirectory.dir(".build/${info.triple}/debug"))

        workingDir = layout.projectDirectory
        // 実行するものとしてswiftlyのpathを指定
        executable(getSwiftlyPath())
        // ビルドコマンドの設定 --swift-sdkを指定(Swift SDK for Androidの指定)
        args("run", "swift", "build", "+${swiftVersion}", "--swift-sdk", info.triple)
    }

    buildSwiftAll.configure { dependsOn(task) }
}

SwiftだけでAndroidアプリを動かす

一応サンプルにはUIも含めSwiftで実装しているAndroidアプリもあるので、その構造や仕組みを見てみる。
「あれ?最初にSwift SDK for AndroidではUIはSwiftで置き換えないって言ったじゃん?」と思うだろうが、これは一応できるけど推奨されてない方法だと思う。

native-activityサンプルでは、swift-javaを使わず、Android NDKの力を活用して動かしている。

↓簡単にまとめるとこんな感じ

  • 描画はOpenGL ES(Cベースの描画API)を利用
  • Android Native App Glue frameworkを利用してAndroidのライフサイクルを取得して利用

構成

swift-android-examplesをみると、native-app-glueとnative-activityでフォルダが分かれている。

native-app-glue

これ自体はPackage。native-activity側からAndroidNativeAppGlueというPackage名で呼ばれている

  • Source
    • AndroidNativeAppGlue
      • Android NDKが提供しているframework
      • NativeActivityのライフサイクルイベントとタップ入力などを管理可能にする。
      • android_native_app_glue.cや.hをみるとヘッダーにAOSPのCopyrightがあり、AOSPのミラーをみると全く同じファイルがある
    • AndroidOpenGL
      • OpenGL ESを呼び出すためのヘッダーを定義
      • OpenGL ESはAndroid固有のものではないので中身がAndroid NDKに存在するというわけではない。

native-app

  • native-activity.swift
    • Swiftで実装されたNative Activity
    • AndroidNativeAppGlueに依存
    • OpenGL ESを用いて色が継続的に変化するように描画の定義
    • NativeActivityのライフサイクルを参照してセットアップやリソース解放など

コードを見てみる

実際にライフサイクルを参照している箇所をみると、AndroidNativeAppGlueを用いてライフサイクルを参照し、それぞれに応じたイベントを発火している。

// native-activity.swift

public func android_main(_ app: UnsafeMutablePointer<android_app>) {
   ...
        switch Int(cmd) {
        case APP_CMD_INIT_WINDOW:
            // Window初期化時
            // OpenGL ESの設定や初期化
            _ = engine.initDisplay()
        case APP_CMD_TERM_WINDOW:
            // Window終了時
            // 描画ループの停止やリソース解放
            engine.termDisplay()
        case APP_CMD_GAINED_FOCUS:
            // アプリ起動後や戻ってきた時
            // 描画ループの開始
            engine.resume()
        case APP_CMD_LOST_FOCUS:
            // アプリがbackgroundに行った時など
            // 描画ループの停止
            engine.pause()
        default:
            print("Unsupported command \(cmd)")
        }
  ...
}

低レベルのAPIを操作するので全体的にポインタでゴリゴリやっている感じ。
Swiftだけで完全にアプリを作るのはSwift SDK for Androidでは難しいと思えた。

要点まとめ

  • Swift SDK for Androidって何?
    • Swiftを.soにビルドしてJava/Kotlinから呼び出すためのツール
    • ビジネスロジックを共通化するのが目的
  • swift-javaによって以下を生成
    • .soを呼び出すためのJava Wrapper
    • Swiftを呼び出すためのJNI Binding
  • AndroidアプリからSwiftを呼び出す仕組み
    • Java/Kotlin → Java Wrapper → .so内のJNIバインディング → Swiftコード

最後に

公式が動いてるのと、Skipの創設メンバーもAndroid Workgroupの主軸で動いてるので中途半端に終わることは無いと思う。なのでうまくいけば今後KMPみたいな立ち位置になるんじゃないかと期待している。
Skipを普段から使ってる身からすると、UIは共通化するべきではないという気持ちが強くなってきたので期待。
Swift Forumで議論してるのと、新しい投稿はAndroidカテゴリで見ることができるので、最新情報を追いたい方は見てみるのが良い。

何か間違いなどあればコメントくださいmm

参考

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?