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
- Any tips for reducing FoundationICU size (~30MB)?
- Has anyone used swift-java's auto-generated bindings in production vs manual JNI?
- Best practices for CI/CD with cross-platform Swift builds?
Happy to share more details or answer questions!