Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android doesn’t deserve Swift… But we did it a...

Avatar for Pierluigi Cifani Pierluigi Cifani
September 19, 2025
130

Android doesn’t deserve Swift… But we did it anyway 🤷

Talk given in NSSpain 2025

Avatar for Pierluigi Cifani

Pierluigi Cifani

September 19, 2025
Tweet

Transcript

  1. Mobile Lead @ TheLeftBit I ’ m Pierluigi 👋 @piercifani

    @piercifani linkedin.com/in/pcifani theleftbit.com
  2. Swift on Android Slow to non-existent progress during the first

    years • Community-led, not Apple-driven. • No official Android SDK for Swift exists. • Patches have to be applied on every official Swift Release to keep it compiling.
  3. Foundation.framework UserDefaults Date Operation URLSession DateFormatter Calendar NotificationCenter JSONEncoder Data

    URL URLRequest FileManager Bundle Formatter Locale UUID OperationQueue ProcessInfo Timer IndexSet
  4. • Toolchain built on top of the Open Source Swift

    project. • Allows you to write Android apps and libraries in Swift. • No compromise solution for iOS apps. • Not the only Swift-on-Android toolchain out there! Skip.tools
  5. • Projects are heavily modularized using SPM. • Swift 6

    compatible. • Relies on Swift Concurrency: • No Combine or RxSwift • No GCD • No OperationQueue • Good candidate for Skip ’ s Compiled-mode Starting Point For Naturitas.app
  6. Build with the Skip Toolchain • Build the Module for

    macOS: • You ’ ll have IDE support. • Understand your dependencies. • Refactor UIKit or SwiftUI code out. First, macOS
  7. Build for Android • Most issues fall into three big

    buckets : • Open Source Foundation. • Non-compatible dependencies. • Wrapping Apple frameworks.
  8. import Foundation func sendNetworkRequest() async throws { let url =

    URL(string: "https://www.apple.com/iphone/")! let urlRequest = URLRequest(url: url) let _ = try await URLSession.shared.data(for: urlRequest) } Build for Android Open Source Foundation
  9. import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif func sendNetworkRequest() async

    throws { let url = URL(string: "https://www.apple.com/iphone/")! let urlRequest = URLRequest(url: url) let _ = try await URLSession.shared.data(for: urlRequest) } Build for Android Open Source Foundation
  10. var targetDependencies: [Target.Dependency] = [ .product( name: "KeychainAccess", package: "KeychainAccess",

    condition: .when(platforms: [.iOS]) ), ] Build for Android Non-compatible dependencies
  11. Build for Android • At this point, the code should

    compile. • But runtime behavior of some classes is not guaranteed to be the same. • For example: UserDefaults does not persist it ’ s values. • Skip has Open Source packages to fill in the gaps. • Enter: Skip Fuse
  12. import PackageDescription var packageDependencies: [Package.Dependency] = […] var targetDependencies: [Target.Dependency]

    = […] packageDependencies.append(contentsOf: [ .package(url: "https://source.skip.tools/skip-fuse.git"), ]) targetDependencies.append(contentsOf: [ .product(name: "SkipFuse", package: "skip-fuse"), ])
  13. Calling Swift from Kotlin • In order to call Swift

    code from Kotlin in Android Studio, we must first create JNI bindings. • WTF is JNI? • “It allows Java code that runs inside a Java Virtual Machine (VM) to interoperate with applications and libraries written in other programming languages”
  14. Calling Swift from Kotlin • There are some limitations when

    creating Kotlin bridges for your Swift code: • Swift idioms that are not available on Kotlin • Skip limitations
  15. Back to Xcode! public class Foo { public init() async

    throws { … } } Kotlin limitations
  16. Back to Xcode! public class Foo { // SKIP @nobridge

    public init() async throws { … } } Kotlin limitations
  17. Back to Xcode! public class Foo { // SKIP @nobridge

    public init() async throws { … } #if os(Android) public static func create() async throws -> Foo { return try await Foo() } #endif } Kotlin limitations
  18. Back to Exporting • All errors and warnings are cleared,

    call: skip export --module NaturitasCore \ -d android/lib/debug/ \ --project naturitas-core \ --no-export-project \ --debug \ --arch aarch64 --arch armv7 --arch x86_64
  19. Android Studio • Based on JetBrain ’ s IntelliJ IDEA.

    • Installs the basic Android toolchain: • Java + Gradle • Android SDK • Emulators • Not AppKit-based, but Swing-Java based. • If you like “Mac-assed” apps, you ’ ll hate it. Everyone ’ s favorite IDE
  20. Android Studio • Place the .aars generated by skip export

    somewhere where the Android codebase can reference it. • You can use: • Custom Maven repository • Github Packages • Merge the iOS and Android repositories • Storing .aars with Git LFS Let ’ s import the .aars
  21. dependencies { // if you keep AARs under app/lib/debug implementation(

    fileTree( mapOf( "dir" to "../lib/debug", "include" to listOf("*.aar") ) ) ) }
  22. Android Studio • Initialize Swift and Skip ’ s runtimes:

    And use the code! class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { … } } }
  23. Android Studio • Initialize Swift and Skip ’ s runtimes:

    And use the code! class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) skip.foundation.ProcessInfo.launch(this) enableEdgeToEdge() setContent { … } } }
  24. Android Studio • Now import and use your Swift code

    from Kotlin! And use the code! import naturitas.core.AuthenticationManager val authManager = AuthenticationManager.create( apiClient = UserAPIClient.create(environment), socialNetworkTokenProvider = TokenRefresher() )
  25. Android Studio • There ’ s no throws in Java/Kotlin.

    Anything can throw so nothing throws. • Remember to try catch when the Swift code you ’ re calling can throw. • Add logging. A LOT. • You can ’ t add breakpoints inside any precompiled code; and your Swift Packages are integrated like that. Some tips
  26. Android Studio • Do not expect deterministic memory behavior. •

    Java ’ s Garbage Collector can come and go whenever it wants. • Lots of patience when interacting with: • Hilt • Proguard Some tips
  27. Android + Skip • Adds at least +50MB to the

    App ’ s Binary. • Iterate cycle depends on re-exporting the Swift codebase, which is slow. • Cultural friction. • Android Developers will complain. • It helped us the fact that I could put my thumb in the scale and make executive decisions. Tradeoffs
  28. Android + Skip • URLSession has limits: • Bigger than

    usual errors where reported. • Based on libCURL • Not visible on Android Studio network debugger • We replaced it for an OKHTTP-based wrapper. Found issues
  29. Android + Skip • All of the issues that you

    will encounter can be workaround or fixed. It ’ s just Swift code! • Dependencies can be forked and patched • Or even inverted! Conclusions
  30. Android + Skip • We in the process of rewriting

    layer by layer the Naturitas app. • Replacing Kotlin-based subsystems by Swift-based subsystems. • Deeplinks, Push Notifications, Authentication and Remote Config ✅ • Networking and Data Validation ⏳ • Cart and Checkout ⏭ End result
  31. Common UI Elements AsyncButton("Hola 🇪🇸") { try await Task.sleep(for: .seconds(1))

    } .buttonStyle(.borderedProminent) .asyncButtonLoadingConfiguration( style: .inline(tint: .red) )
  32. Common UI Elements AsyncButton("Hola 🇪🇸") { try await Task.sleep(for: .seconds(1))

    } .buttonStyle(.borderedProminent) .asyncButtonLoadingConfiguration( style: .blocking( dimsBackground: true, successMessage: “Hecho” ) )
  33. Common UI Elements @State var asyncViewID: String = "swap-1" VStack

    { Picker("Selection", selection: $asyncViewID) { … } .pickerStyle(.segmented) .padding(.horizontal) Spacer() AsyncView( id: $asyncViewID, dataGenerator: { try await generateData(asyncViewID) }, hostedViewGenerator: { ContentView(data: $0) } ) }
  34. • Partner with JetBrains to ensure Swift Package Manager integration

    is seamless across the IDE. • Collaborate with Google to minimize any binary size overhead when adding Swift to Android projects. • Keep investing in Swift-Java to make it the de- facto way to interoperate with JVM languages.
  35. Support the efforts to take Swift outside of the Apple

    ecosystem in any way that we can.
  36. @ fi nagol fi n @vgorloff @andriydruk @compnerd @Obbut @aabewhite

    @etcwilde @Joannis @lhoward @marcprux Thank you!