Firebase
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.
Package
Section titled “Package”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.
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)}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:
#if os(Android)import SkipFirebaseCore#elseimport FirebaseCore#endif
...
/* 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:
#if os(Android)import SkipFirebaseFirestore#elseimport FirebaseFirestore#endif
...
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 { ... }
...}Messaging
Section titled “Messaging”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 ...>...<serviceandroid:name="skip.firebase.messaging.MessagingService"android:exported="false"><intent-filter><action android:name="com.google.firebase.MESSAGING_EVENT" /></intent-filter></service><meta-dataandroid: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#if os(Android)import SkipFirebaseMessaging#elseimport FirebaseMessaging#endif
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:
#if os(Android)import SkipFirebaseCore#elseimport FirebaseCore#endif
...
/* 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() }
...}...
class AppMainDelegate: NSObject, AppMainDelegateBase { ...
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { AppDelegate.shared.onLaunch() application.registerForRemoteNotifications() // <-- Insert return true }
...}...
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!
Error handling
Section titled “Error handling”Firestore
Section titled “Firestore”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 { ... }}Catching other errors
Section titled “Catching other errors”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 }}Testing
Section titled “Testing”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 SkipFirebaseCoreimport SkipFirebaseAuthimport SkipFirebaseStorageimport SkipFirebaseDatabaseimport SkipFirebaseAppCheckimport SkipFirebaseFunctionsimport SkipFirebaseFirestoreimport SkipFirebaseMessagingimport SkipFirebaseCrashlyticsimport SkipFirebaseRemoteConfigimport SkipFirebaseInstallations
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 hereapp.isDataCollectionDefaultEnabled = false
// use the app to create and test serviceslet 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)Common Errors
Section titled “Common Errors”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.