This repository shows one shared Swift business-logic implementation used by both iOS and Android.
The shared tap-counter logic lives in TapCounterSwiftLib, the iOS app consumes that package directly, and the Android side uses TapCounterAndroidLib as a JNI/export bridge that generates Java bindings and .so artifacts through swift-java.
This repository accompanies a two-part walkthrough showing how to build and then use a shared Swift 6.3 codebase across iOS and Android:
- Part 1:
Build the Android
.sofrom Swift 6.3 with Swift Package Manager. Watch Part 1 - Part 2:
Wire the generated
.sointo the Android app, keep iOS consuming the shared Swift package directly, and explain the architecture and integration issues. Watch Part 2
If you are starting from scratch, watch Part 1 first. Part 2 assumes the native Android artifact generation flow is already in place.
TapCounterSwiftLib/The single source of truth for the sharedTapCounterlogic used by both apps.TapCounterAndroidLib/Android interop package. It depends onTapCounterSwiftLiband usesswift-javaplusJExtractSwiftPluginto generate Java bindings and Android native libraries.TapCounter_Android/Kotlin/Jetpack Compose Android app that loads the generated.sofiles and calls the generated Java wrapper classes.TapCounter_iOS/SwiftUI iOS app that consumes the shared Swift package directly through Swift Package Manager.
TapCounterSwiftLib contains the real implementation of the tap counter:
TapCountertap()reset()currentCountlabel()
This package is intended to stay platform-safe and reusable.
TapCounterAndroidLib exists only to expose the shared Swift logic to Android:
- it depends on
TapCounterSwiftLib - it depends on
swift-java - it uses
JExtractSwiftPlugin - it exports a thin bridge type for JNI generation
The bridge target should not own business logic. It only adapts the shared Swift package for Android/native interop.
This separation is the key architectural choice shown in Part 2:
TapCounterSwiftLibstays clean and Swift-first for Apple platformsTapCounterAndroidLibstays Android-specific and owns the interop/export concerns- the business logic remains shared, even though the delivery mechanism differs by platform
The Android app integrates with the generated native layer in two parts:
- native libraries are staged automatically during build by Gradle
- generated Java bindings are referenced directly from
TapCounterAndroidLib/.build/...
The app currently uses the generated TapCounterBridge wrapper from:
com.tonytrejo.TapCounterAndroidLib.TapCounterBridge
and loads:
SwiftJavaTapCounterAndroidLib
from MainActivity.
Part 1 focuses on producing the Android native output from Swift 6.3:
- Keep the shared business logic in
TapCounterSwiftLib. - Use
TapCounterAndroidLibas the Android-facing bridge package. - Generate the Android
.soartifact and Java bindings withswift-java. - Treat the generated output under
.build/...as the source for Android integration.
Part 2 shows the full end-to-end usage:
- The iOS app imports
TapCounterSwiftLibas a local Swift package. - The Android app uses Jetpack Compose for UI, but calls into Swift through
TapCounterBridge. - Gradle stages the generated
.soplus its native dependencies into the Android app. - Both apps end up running the same counter behavior from the same Swift implementation.
The Android app no longer relies on manually copied runtime .so files in app/src/main/jniLibs.
Instead, app/build.gradle.kts defines a Gradle Sync task named syncSwiftNativeLibs that gathers:
libTapCounterAndroidLib.solibSwiftJava.so- Swift runtime
.sofiles from the installed Swift Android SDK libc++_shared.sofrom the Android NDK bundled with that SDK
These are staged into:
TapCounter_Android/app/build/generated/jniLibs
and the app’s jniLibs source set points at that generated directory.
This means:
- no manual copying is required for runtime
.sofiles - the Android APK still packages all required native dependencies
- the build stays reproducible as long as the Swift Android SDK is installed locally
swift-java generates Java wrapper classes for the Android bridge package under:
TapCounterAndroidLib/.build/plugins/outputs/.../src/generated/java
The Android app references that generated source path directly, so when the Swift bridge type changes and bindings are regenerated, Android Studio can pick up the new generated classes without relying on stale copied wrapper files.
- Update shared counter behavior in
TapCounterSwiftLib. - Keep
TapCounterAndroidLibas a bridge-only package for Android export. - Regenerate Android bindings and
.soartifacts fromTapCounterAndroidLib. - Run the iOS app, which imports the shared Swift package directly.
- Build the Android app, which stages native libraries with Gradle, compiles against generated Java bindings, and uses the Swift-backed tap counter from Compose.
TapCounterSwiftLibis the only place where counter business logic should live.TapCounterAndroidLibis bridge/export glue, not a second logic implementation.TapCounter_iOSandTapCounter_Androidare intentionally thin host apps around the same shared logic.- The Android setup depends on a local Swift Android SDK installation. The Gradle script currently defaults to:
~/Library/org.swift.swiftpm/swift-sdks/swift-6.3-RELEASE_android.artifactbundle/swift-android - If needed, that path can be overridden with the Gradle property:
swiftAndroidSdkRoot
Error:
java.lang.UnsatisfiedLinkError: dlopen failed: library "liblibSwiftJava.so" not found
Cause:
System.loadLibrary(...)was called with thelibprefix included.
Wrong:
System.loadLibrary("libSwiftJava")
System.loadLibrary("libTapCounterAndroidLib")Correct:
System.loadLibrary("SwiftJava")
System.loadLibrary("TapCounterAndroidLib")Android automatically adds the lib prefix and .so suffix.
Errors seen:
libswiftSwiftOnoneSupport.so not foundlibswiftCore.so not foundlibswift_Concurrency.so not found- similar
UnsatisfiedLinkErrormessages for Swift runtime.sofiles
Cause:
libSwiftJava.soandlibTapCounterAndroidLib.sodepend on additional Swift runtime libraries that must be packaged into the APK.
Fix:
- use the Gradle
syncSwiftNativeLibstask in app/build.gradle.kts - stage Swift runtime
.sofiles from the installed Swift Android SDK intobuild/generated/jniLibs
Error:
java.lang.UnsatisfiedLinkError: dlopen failed: library "libc++_shared.so" not found
Cause:
- the Swift/NDK native libraries also depend on the Android C++ shared runtime.
Fix:
- package
libc++_shared.sofrom the Android NDK bundled in the Swift Android SDK - this is handled automatically by the Gradle
syncSwiftNativeLibstask
Symptoms:
- Android Studio still shows
TapCounterafter the Swift bridge was renamed toTapCounterBridge - imports stay red even though generated bindings were rebuilt
Cause:
- the app was pointing at copied generated Java files under
app/src/main/java/... - regenerating bindings in
TapCounterAndroidLib/.build/...does not automatically update copied files
Fix:
- stop copying generated Java wrapper files
- point the Android app directly to:
TapCounterAndroidLib/.build/plugins/outputs/.../src/generated/java - this is now configured in app/build.gradle.kts
Errors seen:
package jdk.jfr does not existcannot find symbol class Labelcannot find symbol class Description
Cause:
- some
SwiftKitCoreannotation classes fromswift-javaimportedjdk.jfr.*, which is not available on Android
Fix:
- keep Android-safe local versions of:
org.swift.swiftkit.core.annotations.ThreadSafeorg.swift.swiftkit.core.annotations.Unsigned
- compile the app against the local Android-compatible versions
Symptoms:
- Xcode shows
No such module 'TapCounterSwiftLib' - the package dependency appears with a
?
Cause:
- Xcode package resolution can get stale or confused even when SwiftPM itself resolves the local dependency correctly
Fixes that help:
- Reset package caches in Xcode
- Resolve package versions again
- Reopen the package root instead of only opening an individual Swift file
- If needed, delete:
TapCounterAndroidLib/.buildTapCounterAndroidLib/.swiftpm
- Use the canonical package identity in
Package.swift
Error:
target 'TapCounterAndroidLib' referenced in product 'TapCounterAndroidLib' is empty
Cause:
- the target only had
swift-java.configand no Swift source files
Fix:
- add a real Swift source file to
TapCounterAndroidLib - keep that file bridge-only
- delegate all real logic to
TapCounterSwiftLib
Goal:
- one logic implementation for both iOS and Android
How this repository handles it:
TapCounterSwiftLibowns the only realTapCounterlogicTapCounterAndroidLibcontains only a bridge/export surface for JNI generation- Android uses the generated Java wrapper around that bridge
This means there is one business-logic implementation, even though Android still needs a thin exported bridge type for tooling reasons.
%20%E2%80%94%20Part%202.png)