This package provides Firebase support for Skip Swift projects.
The Swift side uses the official Firebase iOS SDK directly,
with the various SkipFirebase* modules passing the transpiled calls
through to the Firebase Android SDK.
For an example of using Firebase in a Skip Fuse app, see the FiresideFuse Sample. For a Skip Lite app, see the Fireside Sample.
The modules in the SkipFirebase framework project mirror the division of the SwiftPM modules in the Firebase iOS SDK (at https://github.com/firebase/firebase-ios-sdk.git), which is also mirrored in the division of the Firebase Kotlin Android gradle modules (at https://github.com/firebase/firebase-android-sdk.git).
See the Package.swift files in the
FiresideFuse and Fireside apps for examples of integrating Firebase dependencies.
skip-firebase exposes Swift API parity with the Firebase iOS SDK for the most common use cases on Android. The Android-side implementation lives in each Sources/SkipFirebase* Swift file (transpiled to Kotlin) and forwards calls to the official Firebase Android SDK. The table below summarizes, per module, the approximate share of the iOS API surface that has been bridged, along with the major gaps.
| Module | iOS API Coverage | Highlights | Notable Gaps |
|---|---|---|---|
SkipFirebaseCore |
~80% | FirebaseApp.configure (default + named + custom FirebaseOptions), app lifecycle, Timestamp, deep Kotlin↔Swift value conversion helpers |
Custom FirebaseLogger, advanced data-collection toggles |
SkipFirebaseFirestore |
~80% | Collections, documents, batched writes, queries, Filter.and/.or composition, FieldPath, aggregate queries (count/average/sum), snapshot listeners, FieldValue sentinels, LoadBundleTaskProgress, errors mapped to NSError with FirestoreErrorDomain/FirestoreErrorCode |
runTransaction (Transaction is currently a passthrough wrapper), Codable document encoding/decoding, GeoPoint, FirestoreSettings/cache configuration, InputStream-based bundle loading |
SkipFirebaseAuth |
~60% | Email/password sign-in, anonymous sign-in, email-link sign-in, interactive OAuth provider sign-in (Activity-based), state-change listener, ID-token retrieval, profile changes, account linking, reauthentication, fetchSignInMethods, ActionCodeSettings, partial AuthErrorCode mapping |
Phone auth (PhoneAuthProvider, SMS verification), multi-factor (MultiFactor/MultiFactorResolver), applyActionCode/checkActionCode/confirmPasswordReset/verifyPasswordResetCode, custom-token sign-in, GameCenterAuthProvider, language/tenant configuration, updateEmail/updatePassword |
SkipFirebaseStorage |
~80% | Bucket and reference resolution, upload (putFile/putData in both callback and async forms), download (getData/write(toFile:)), StorageMetadata read/write, downloadURL, delete, pause/resume/cancel on uploads, list/listAll with pagination (pageToken), live Progress snapshot observers on uploads and file downloads |
Full StorageError code mapping, putStream/putString |
SkipFirebaseMessaging |
~70% | FCM token retrieval + auto-refresh, topic subscribe/unsubscribe, MessagingDelegate, MessagingService integrating with iOS-style UNUserNotificationCenterDelegate, intent routing, localized loc_key/loc_args title/body, RemoteMessage → UNNotification translation |
Per-sender FCM token APIs and notification-service-extension helpers (populateNotificationContent, exportDeliveryMetricsToBigQuery) are stubbed out as @available(*, unavailable) |
SkipFirebaseAnalytics |
~70% | logEvent, user properties/ID, consent (setConsent with ConsentType/ConsentStatus), session ID, setSessionTimeoutInterval, setDefaultEventParameters, appInstanceID, all standard event/parameter/user-property name constants, SwiftUI .analyticsScreen modifier |
initiateOnDeviceConversionMeasurement (email/phone variants), handleEvents(forSession:), deep-link helpers |
SkipFirebaseRemoteConfig |
~70% | fetch/activate/fetchAndActivate, ensureInitialized, value retrieval, keys-by-prefix, RemoteConfigSettings (intervals + timeout), defaults, RemoteConfigValue (string/bool/number/data/json/source) |
addOnConfigUpdateListener (real-time config updates), setCustomSignals |
SkipFirebaseAppCheck |
~60% | Token retrieval (token(forcingRefresh:), limitedUseToken), token-change listener bridged through NotificationCenter, debug provider factory, AppCheckErrorCode |
iOS-only DeviceCheckProviderFactory/AppAttestProviderFactory (no Android equivalent), custom AppCheckProvider implementations |
SkipFirebaseCrashlytics |
~75% | log, setCustomValue/setCustomKeysAndValues, setUserID, didCrashDuringPreviousExecution, checkForUnsentReports/sendUnsentReports/deleteUnsentReports, record(error:userInfo:), recordExceptionModel, FIRExceptionModel/FIRStackFrame |
logWithFormat and checkAndUpdateUnsentReports are marked @available(*, unavailable); no per-FirebaseApp instance accessor |
SkipFirebaseFunctions |
~45% | Default/regional/emulator instance, httpsCallable(_:), completion-based call, automatic Kotlin→Swift result conversion via deepSwift |
async/await call signatures, streaming RPCs, per-call options (HTTPSCallableOptions), explicit call timeouts |
SkipFirebaseDatabase |
~5% | Database.database() / Database.database(app:) singleton accessors |
Effectively the entire API: DatabaseReference, child/push/setValue/updateChildValues/removeValue, observe/observeSingleEvent, queries, DataSnapshot, ServerValue, online/offline toggle |
SkipFirebaseInstallations |
~75% | Singleton accessor (installations()/installations(app:)), installationID(), authToken(), authTokenForcingRefresh(_:), delete(), InstallationsAuthTokenResult (authToken, expirationDate) |
installationIDDidChangeNotification, per-FirebaseApp notification keys |
SkipFirebasePerformance |
~65% | Performance.sharedInstance(), isDataCollectionEnabled, trace(name:), HTTPMetric(url:httpMethod:), full Trace API (start/stop, incrementMetric, valueForMetric, attributes), full HTTPMetric API (start/stop, responseCode, payload sizes, content type, attributes), HTTPMethod enum |
isInstrumentationEnabled (no Android equivalent, marked unavailable), Performance.startTrace(name:) static convenience, per-FirebaseApp instance accessor |
Overall coverage across the fourteen modules: roughly 60% of the iOS API surface. The most production-used modules — Core, Firestore, Auth, Storage, Messaging, Analytics, Crashlytics, and RemoteConfig — sit in the 60–80% range and cover the standard read/write/sign-in/log/notify paths. SkipFirebaseDatabase (Realtime Database) is mostly a stub.
- Firestore is the most complete module: all common CRUD, queries (including
Filtercombinators andFieldPathpredicates), snapshot listeners (withMetadataChangessupport), batched writes, aggregate queries, andFieldValuesentinels round-trip correctly between Swift and the Firestore Android SDK. Errors are mapped toNSErrorwithFirestoreErrorDomain/FirestoreErrorCodeso error handling looks the same on both platforms. - Auth covers the most common sign-in flows (email/password, anonymous, interactive OAuth via the system browser, email-link) and exposes a familiar
UserAPI with profile changes, account linking, reauthentication, ID-token retrieval, and a state-change listener. - Messaging handles the full FCM lifecycle: token retrieval and auto-refresh, topic subscribe/unsubscribe,
RemoteMessage→UNNotificationtranslation (including localizedloc_key/loc_argssubstitution from the app'sLocalizable.strings), and aMessagingServicethat integrates with iOS-styleUNUserNotificationCenterDelegate. - Storage supports both data-based and file-based uploads/downloads in callback and
asyncforms, plus fullStorageMetadataread/write. - Analytics, RemoteConfig, AppCheck, and Crashlytics all expose the workhorse APIs that apps need day-to-day, including a SwiftUI
.analyticsScreenmodifier and aNotificationCenter-backed AppCheck token-change listener.
- Realtime Database (
SkipFirebaseDatabase) is currently only a stub.DatabaseReference,observe/observeSingleEvent, queries, and writes have not been bridged. Apps that rely on the Realtime Database should use Firestore instead or contribute the missing wrappers. - Phone-number authentication and multi-factor flows in
SkipFirebaseAuthare not bridged. Apps requiring SMS verification or MFA need to drop into#if SKIPblocks and call the Android Firebase SDK directly. runTransactionin Firestore is not implemented — theTransactionclass is currently a passthrough wrapper with no operations. Multi-document atomic reads-then-writes need to be expressed asWriteBatchcommits or as Kotlin-side code today.SkipFirebaseFunctionslacksasync/awaitcall signatures, streaming RPCs, and per-call options. Only completion-based callback calls are supported.SkipFirebaseRemoteConfigdoes not yet exposeaddOnConfigUpdateListener(real-time config updates) or custom signals.SkipFirebaseAppCheckships only theDebugprovider factory; custom provider implementations are not yet bridgeable.SkipFirebaseInstallationscannot bridge theInstallationIDDidChangenotification or theInstallationIDDidChangeAppNameKeyuserInfo key. The Firebase Android SDK has no equivalent push-notification mechanism for installation ID changes — it exposes no broadcast, callback, or listener that fires when the ID rotates. As a result, any code that observesNotificationCenter.default.publisher(for: .InstallationIDDidChange)on iOS will compile on Android but never receive an event. If your app relies on this to, for example, re-upload the installation ID to your backend after it changes, you will need an alternative strategy on Android (such as fetching the ID on every app foreground, or polling after Firebase SDK upgrades).
The following Firebase iOS SDK products do not yet have a SkipFirebase* counterpart in this package: FirebaseABTesting, FirebaseAI / Vertex AI, FirebaseAppDistribution, FirebaseDataConnect, FirebaseDynamicLinks (deprecated upstream), FirebaseInAppMessaging, and FirebaseMLModelDownloader. Pull requests adding any of these are welcome.
For a Skip app, the simplest way to setup Firebase support is to
create a Firebase project at https://console.firebase.google.com/project.
Follow the Firebase setup instructions to obtain the
GoogleService-Info.plist and google-services.json files and
add them to the iOS and Android sides of the project:
- The
GoogleService-Info.plistfile should be placed in theDarwin/folder of the Skip project - The
google-services.jsonfile should be placed in theAndroid/app/folder of the Skip project
In addition, the com.google.gms.google-services plugin will need to be added to the
Android app's Android/app/build.gradle.kts file in order to process the google-services.json
file for the app, like so:
plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.android.application)
id("skip-build-plugin")
id("com.google.gms.google-services") version "4.4.4" apply true
id("com.google.firebase.crashlytics") version "3.0.2" apply true # (if using Crashlytics)
}For concrete examples, see the FireSideFuse Sample project. {: class="callout info"}
Once Firebase has been added to your project, you need to configure the FirebaseApp on app startup. This is typically done in the onInit() callback of the *AppDelegate in your *App.swift file. Here is a snippet from the FireSideFuse sample app:
import SkipFirebaseCore
...
/* SKIP @bridge */public final class FireSideFuseAppDelegate : Sendable {
/* SKIP @bridge */public static let shared = FireSideFuseAppDelegate()
...
/* SKIP @bridge */public func onInit() {
logger.debug("onInit")
FirebaseApp.configure()
...
}
...
}After configuring the FirebaseApp, you will be able to access the singleton type for each of the
imported Firebase modules. For example, the following actor uses the Firestore singleton:
// Sources/FireSideFuse/FireSideFuseApp.swift
import SkipFirebaseFirestore
...
public actor Model {
/// The shared model singleton
public static let shared = Model()
private let firestore: Firestore
private init() {
self.firestore = Firestore.firestore()
}
public func queryData() async throws -> [DataModel] { ... }
public func saveData(model: DataModel) async throws { ... }
...
}After setting up your app to use Firebase, enabling push notifications via Firebase Cloud Messaging (FCM) requires a number of additional steps.
-
Follow Firebase's instructions for creating and uploading your Apple Push Notification Service (APNS) key.
-
Use Xcode to add the Push capability to your iOS app.
-
Add Skip's Firebase messaging service and default messaging channel to
Android/app/src/main/AndroidManifest.xml:... <application ...> ... <service android:name="skip.firebase.messaging.MessagingService" android:exported="false"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter> </service> <meta-data android:name="com.google.firebase.messaging.default_notification_channel_id" android:value="tools.skip.firebase.messaging" /> </application>
-
Consider increasing the
minSdkversion of your Android app. Prior to SDK 33, Android does not provide any control over asking the user for push notification permissions. Rather, the system will prompt the user for permission only after receiving a notification and opening the app. Increasing yourminSdkwill allow you to decide when to request notification permissions. To do so, edit yourAndroid/app/build.gradle.ktsfile and change theminSdkvalue to 33. -
Define a delegate to receive notification callbacks. In keeping with Skip's philosophy of transparent adoption, both the iOS and Android sides of your app will receive callbacks via iOS's standard
UNUserNotificationCenterDelegateAPI, as well as the Firebase iOS SDK'sMessagingDelegate. Here are example Skip Fuse delegate implementations that works across both platforms:
import SwiftFuseUI
import SkipFirebaseMessaging
final class NotificationDelegate : NSObject, UNUserNotificationCenterDelegate, Sendable {
public func requestPermission() {
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
Task { @MainActor in
do {
if try await UNUserNotificationCenter.current().requestAuthorization(options: authOptions) {
logger.info("notification permission granted")
} else {
logger.info("notification permission denied")
}
} catch {
logger.error("notification permission error: \(error)")
}
}
}
public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
let content = notification.request.content
logger.info("willPresentNotification: \(content.title): \(content.body) \(content.userInfo)")
return [.banner, .sound]
}
public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
let content = response.notification.request.content
logger.info("didReceiveNotification: \(content.title): \(content.body) \(content.userInfo)")
#if os(Android) || !os(macOS)
// Example of using a deep_link key passed in the notification to route to the app's `onOpenURL` handler
if let deepLink = response.notification.request.content.userInfo["deep_link"] as? String, let url = URL(string: deepLink) {
Task { @MainActor in
await UIApplication.shared.open(url)
}
}
#endif
}
}
// Your Firebase MessageDelegate must bridge because we use the Firebase Kotlin API on Android.
/* SKIP @bridge */final class MessageDelegate : NSObject, MessagingDelegate, Sendable {
/* SKIP @bridge */public func messaging(_ messaging: Messaging, didReceiveRegistrationToken token: String?) {
logger.info("didReceiveRegistrationToken: \(token ?? "nil")")
}
}- Wire everything up. This includes assigning your shared delegate, registering for remote notifications, and other necessary steps. Below we build on our previous Firebase setup code to perform these actions. This is taken from our FireSideFuse sample app:
// Sources/FireSideFuse/FireSideFuseApp.swift
import SkipFirebaseCore
...
/* SKIP @bridge */public final class FireSideFuseAppDelegate : Sendable {
/* SKIP @bridge */public static let shared = FireSideFuseAppDelegate()
private let notificationDelegate = NotificationDelegate()
private let messageDelegate = MessageDelegate()
private init() {
}
/* SKIP @bridge */public func onInit() {
logger.debug("onInit")
// Configure Firebase and notifications
FirebaseApp.configure()
Messaging.messaging().delegate = messageDelegate
UNUserNotificationCenter.current().delegate = notificationDelegate
}
/* SKIP @bridge */public func onLaunch() {
logger.debug("onLaunch")
// Ask for permissions at a time appropriate for your app
notificationDelegate.requestPermission()
}
...
}// Darwin/Sources/Main.swift
...
class AppMainDelegate: NSObject, AppMainDelegateBase {
...
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
AppDelegate.shared.onLaunch()
application.registerForRemoteNotifications() // <-- Insert
return true
}
...
}// Android/app/src/main/kotlin/.../Main.kt
...
open class MainActivity: AppCompatActivity {
...
override fun onCreate(savedInstanceState: android.os.Bundle?) {
...
setContent {
...
}
skip.firebase.messaging.Messaging.messaging().onActivityCreated(this) // <-- Insert
FireSideFuseAppDelegate.shared.onLaunch()
...
}
...
}- See Firebase's iOS instructions and Android instructions for additional details and options, including how to send test messages to your apps!
The FiresideFuse and Fireside projects are great references for seeing complete, working Skip Fuse and Skip Lite apps using Firebase push notifications. {: class="callout info"}
The Firestore API converts com.google.firebase.firestore.FirebaseFirestoreException to NSError so you can handle errors the same way on both platforms:
do {
try await Firestore.firestore().collection("foo").document("bar").updateData(...)
} catch let error as NSError {
if error.domain == FirestoreErrorDomain && error.code == FirestoreErrorCode.notFound.rawValue {
...
}
}Other parts of this library have not been updated to this unified error handling. Instead, you can access the underlying Kotlin exceptions in SKIP blocks according to the documentation:
- FirebaseAuth: https://firebase.google.com/docs/reference/android/com/google/firebase/auth/FirebaseAuth
- FirebaseMessaging.MessagingService.onSendError: https://firebase.google.com/docs/reference/android/com/google/firebase/messaging/SendException
- FirebaseStorage: https://firebase.google.com/docs/reference/android/com/google/firebase/storage/StorageException
do {
try await Storage.storage().reference().child("nonexistent").delete()
} catch {
#if !SKIP
let error = error as NSError
let errorCode = error.domain == StorageError.errorDomain ? error.code : nil
#else
let exception = (error as Exception).cause as? com.google.firebase.storage.StorageException
let errorCode = exception?.code.value()
#endif
if errorCode == -13010 {
// Object not found
}
}For unit testing, where there isn't a standard place to store the
GoogleService-Info.plist and google-services.json configuration files,
you can create an configure the app using the SkipFirebaseCore.FirebaseApp
API manually from the information provided from the Firebase console, like so:
import SkipFirebaseCore
import SkipFirebaseAuth
import SkipFirebaseStorage
import SkipFirebaseDatabase
import SkipFirebaseAppCheck
import SkipFirebaseFunctions
import SkipFirebaseFirestore
import SkipFirebaseMessaging
import SkipFirebaseCrashlytics
import SkipFirebaseRemoteConfig
import SkipFirebaseInstallations
import SkipFirebasePerformance
let appName = "myapp"
let options = FirebaseOptions(googleAppID: "1:GCM:ios:HASH", gcmSenderID: "GCM")
options.projectID = "some-firebase-projectid"
options.storageBucket = "some-firebase-demo.appspot.com"
options.apiKey = "some-api-key"
FirebaseApp.configure(name: appName, options: options)
guard let app = FirebaseApp.app(name: appName) else {
fatalError("Cannot load Firebase config")
}
// customize the app here
app.isDataCollectionDefaultEnabled = false
// use the app to create and test services
let auth = Auth.auth(app: app)
let storage = Storage.storage(app: app)
let database = Database.database(app: app)
let appcheck = AppCheck.appCheck(app: app)
let functions = Functions.functions(app: app)
let firestore = Firestore.firestore(app: app)
let crashlytics = Crashlytics.crashlytics(app: app)
let remoteconfig = RemoteConfig.remoteConfig(app: app)
let installations = Installations.installations(app: app)Error in adb logcat: FirebaseApp: Default FirebaseApp failed to initialize because no default options were found.
This usually means that com.google.gms:google-services was not applied to your gradle project.
The app's com.google.gms:google-services plugin must be applied to the build.gradle.kts file for the app's target.
This software is licensed under the Mozilla Public License 2.0.