How to go about sharing common Swift code between Android and iOS?

Hi all,

I'm absolutely thrilled by the ability to use Swift on Android. I tried out the examples and also succeeded setting up a new project myself from scratch. Well done!

I'm now thinking about the next step, because I want to share code between iOS and Android. For Android we obviously need the swift-java JNI stuff, but not for iOS. How to deal with this? Can I somehow create a pure Swift library package for my shared code, and then use that directly from iOS, but through a JNI-wrapper library package for Android somehow? Or am I on the wrong track here?

Just to further clarify, the structure of a project might be like this:

  • root dir /
    • /android-app (Android app source code)
    • /ios-app (iOS app source code)
    • /jni-wrapper-lib
    • /shared-swift-module-1
    • /shared-swift-module-2

The /jni-wrapper-lib would then be Swift package that somehow "bundles" shared-swift-module-1 and shared-swift-module-2 and makes those available to Android. It wouldn't have any source code itself. iOS would use shared-swift-module-1 and shared-swift-module-2 directly, not the jni-wrapper-lib.

Thanks you for your insights!

P.S. Is @export(implementation) related to this?

5 Likes

Welcome, and no, you are on the right track. The Kotlin/Java code will need JNI bridges to call into Swift, and vice versa. Check out swift-java for both purposes.

1 Like

I am experimenting with the same, and I too have the approach of generating a single shared library target with SwiftPM and attaching the JExtract plugin to multiple targets with code I wish to call from kotlin.

I have two issues with the plugin that I have not yet resolved:

  1. Defining the library name of my plugin targets to be that of the shared library. I can see an option for overriding the library name, but I haven’t made it work.
  2. Figuring out dependencies in JExtract. If one module depends on another and you generate jni-wrappers for both, I can see that JExtract is called with —depends-on. I would guess that this would allow generated code from one module to refer to types from another, but that hasn’t worked for me yet.

Looking forward to hearing about your progress! :blush:

1 Like

Have a look at my branch here GitHub - tkrajacic/swift-android-examples at only-weather-app · GitHub

This is a reduced example of a shared library together with an iOS and an Android project.

In general, this is not how we ultimately want this to work.

The shared package needs to be aware of swift-java. This is really a no-go, since we want to use any swift package really and those should not need modifications to work on Android like this.
Additionally, we can't set the swift-java plugin per platform, because plugins can't (yet) be conditional on target/host platforms. (See the workaround in my Package.swift)

If I remember correctly @ktoso and others are working on a gradle plugin, which would allow you to specify just (straw-man) swiftDependency('MyPackage') in your gradle file, and the plugin would wrap this package (which would be pulled from GitHub for example like any other dependency) in a wrapper package which would contain swift-java. It will be glorious :grinning_face:.

The second big problem is binary size. Even the simplest of packages will pull in Foundation via any of its dependencies and then your binary size will explode to 60+ MB - more likely over 100MB. But this is also something that's being worked on.

In the end, most current problems have solutions on the horizon, and it is easier than ever to get started.

For now, have a look at my branch. Any suggestions and corrections welcome.

4 Likes

Could you give a reference to what’s being done about this? Foundation bloat is a concern of mine too.

(I know a lot of it comes down to ICU, a common pain point for cross-platform libraries.)

You can read about the ICU issue at Android app size and lib_FoundationICU.so. I'm afraid there hasn't been much progress on this front, and short of being able to externalize the ICU data files and "thin" them out with some bespoke tool, I don't know if there is any other realistic solution.

As for the Foundation transitive dependency, @madsodgaard has been doing great work (see The adoption of `import FoundationEssentials` throughout the package ecosystem). It's a bit of a whack-a-mole scenario, though, since any one transitive dependency that does import Foundation will trigger the size inflation.

For now we avoid pulling in Foundation. That's the short story.

If you can't do without "some" Foundation you may get away with import FoundationEssentials as an intermediate step. (It's still wayy too big for our purposes)

I've also found that simply copy-pasting the minimal Foundation type we need into our app has often been fine as an "almost there" step to removing Foundation. Often Foundation types work "standalone" but don't incur the cost of pulling in the entire library.

Again, this is being worked on, but it may still take years before it's the ideal "you only pay for what you use" kind of situation.

If you can already replace Foundation with FoundationEssentials, try linking statically to it and the Swift runtime, plus binary stripping. For that to work, you'll need to build your Swift package as a dynamic library (.so).

It requires some work, and has its quirks, but it'll do wonders to the final .apk footprint. Not sure if SwiftPM allows that kind of customization, though.

2 Likes

I get you, but simply not including Foundation vs. statically linking it is still a night and day difference.

1 Like

Absolutely. I went down that path myself until the cost of redoing JSON encoding and especially Codable reflection became untenable. Depending on the codebase, going Foundation-free can range from convenient/doable to hell on earth.

2 Likes

We use ExtrasJSON, which doesn't bloat the bundle at all really. We even forked it to allow for using it in Swift Embedded mode (embedded branch. You won't get Codable support with Embedded enabled, but without it you do).

It's probably slower than the modern Foundation support, but if you're not parsing GB of JSON it's probably good enough.

I see we're on the same boat with this. :-)

Months ago, I made this one based on Swift Macros rather than Codable, perhaps you find it useful: GitHub - keeshux/swon: Swift Macros for Foundation-free JSON processing. · GitHub

I've been working on a library bindings generation tool that uses the Swift Android toolchain. It allows you to write a library in Swift and generate bindings to use that library in Kotlin on Android (and C#, Dart, and TypeScript actually). That should help with part of the code sharing problem across an iOS and an Android app.

There are a few example libraries that should soon be merged in to the repo too. They show how those libraries are structured, and might help with your directory structure map:

3 Likes