Case Study: Sharing Swift Domain Logic Between iOS and Android with FoodshareCore

Hi everyone,

I wanted to share our experience building a cross-platform Swift library that powers both our iOS and Android apps. This might be useful for others exploring Swift on Android.

Project Overview

FoodshareCore is a shared Swift library containing:

  • Domain models (User, FoodListing, Message, etc.)
  • Validation logic (email, password, input sanitization)
  • Utility functions (distance calculation, date formatting)
  • Business rules

Architecture

FoodshareCore (Swift Package)
├── Sources/FoodshareCore/
│   ├── Models/           # Codable domain models
│   ├── Validation/       # Input validators
│   └── Utilities/        # Shared helpers
└── Tests/

iOS Integration:

import FoodshareCore

let isValid = AuthValidator.validateEmail(email)

Android Integration (via manual JNI):

// Kotlin external declarations call Swift @_cdecl exports
val isValid = FoodshareCoreNative.nativeValidateEmail(email)

Key Learnings

1. NSPredicate Unavailability

We initially used NSPredicate(format:) for regex validation (common iOS pattern). This doesn't work on Android/Linux. Solution: use String.range(of:options:.regularExpression) instead.

// ❌ iOS only
NSPredicate(format: "SELF MATCHES %@", pattern).evaluate(with: string)

// ✅ Cross-platform
string.range(of: pattern, options: .regularExpression) != nil

2. Swift 6 Sendable Challenges

DateFormatter didn't conform to Sendable in older Swift versions. This was fixed in PR #5000 (July 2024). If using older Swift, create formatters locally rather than storing as properties:

// ✅ Create locally in each function
func formatDate(_ date: Date) -> String {
    let formatter = DateFormatter()
    formatter.dateStyle = .medium
    return formatter.string(from: date)
}

3. JNI Integration

We use manual JNI with Swift @_cdecl exports:

// Swift - expose with C linkage
@_cdecl("FoodshareCore_validateEmail")
public func foodshareCore_validateEmail(_ email: UnsafePointer<CChar>?) -> Bool {
    guard let email = email else { return false }
    return AuthValidator.validateEmail(String(cString: email))
}
// Kotlin - external declaration
external fun nativeValidateEmail(email: String): Boolean

Note: swift-java can auto-generate these bindings, but we wrote them manually for finer control.

4. Gradle Configuration

Critical: prevent libdispatch stripping:

packaging {
    jniLibs {
        keepDebugSymbols += "**/*.so"
    }
}

Results

  • Shared validation and utility logic between iOS and Android
  • Identical validation behavior on both platforms
  • Single source of truth for business rules
  • Swift tests validate shared logic (run on macOS, compiled for Android)

Open Questions

  1. Any tips for reducing FoundationICU size (~30MB)?
  2. Has anyone used swift-java's auto-generated bindings in production vs manual JNI?
  3. Best practices for CI/CD with cross-platform Swift builds?

Happy to share more details or answer questions!

5 Likes

Thanks for the writeup! Out of curiosity, which toolchain/SDK are you using? And is your build.gradle[.kts] hand-rolled?

  1. Any tips for reducing FoundationICU size (~30MB)?

This has been a topic of discussion at the thread Android app size and lib_FoundationICU.so for a while. It is certainly a big problem for Swift adoption on Android.

  1. Best practices for CI/CD with cross-platform Swift builds?

If you are using GitHub actions, Swift Android Action · Actions · GitHub Marketplace · GitHub is the de-facto standard, and supports the official Swift SDK for Android as of nightly-6.3.

P.S. If you are willing to share the App/Play Store links for your app(s), it could be nice to add to a draft list of real-world Swift Android apps we're compiling.

Thanks for sharing your experience!

I would nudge you towards giving the swift-java tool a try and file issues if you find anything not flexible enough. Manually writing a binding at first may seem deceptively simple, but can quickly explode in complexity as the project keeps going and you need to access more APIs, not even mentioning the easy-to-get-wrong cdecl signatures when the functions get more complex (or if you’ll need callbacks etc).

We ought to avoid writing bindings by hand and we continue to work on the tool and support libraries, so you’re setting yourself up for additional work (and risk of mistakes/bugs) in the future by starting the “manual” way at this point in time. You’ll also quickly find that more advanced patterns are can be very difficult to handle by manually writing the wrappers… so again, I’d encourage actually giving swift-java a try, and reporting back if you face any issues – we’re here to support these use-cases after all.

We are actively working on getting the java side of the library published but I don’t have a specific timeline to share about this yet sadly, that’ll make it much easier to use in production than it is today.

@madsodgaard AFAIK has been using it successfully at frameo.com already.

Please don’t do this. DateFormatters are expensive to create, so if you’re worried about thread safety it’s better to just wrap the static instance in a lock. Repeated formatter creation is an easy way to get frame drops.

2 Likes