This package provides Firebase support for Skip app/framework 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 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).
An example of a Skip app projects using the Firestore
API at the model layer and the Messaging
API at the app layer can be seen from the command:
skip init --show-tree --icon-color='1abc9c' --no-zero --appid=skip.fireside.App --version 0.0.1 skipapp-fireside FireSide:skip-ui/SkipUI:skip-firebase/SkipFirebaseMessaging FireSideModel:skip-foundation/SkipFoundation:skip-model/SkipModel:skip-firebase/SkipFirebaseFirestore:skip-firebase/SkipFirebaseAuth
This will create an SwiftPM project in skipapp-fireside/Package.swift
like:
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "skipapp-fireside",
defaultLocalization: "en",
platforms: [.iOS(.v16), .macOS(.v13), .tvOS(.v16), .watchOS(.v9), .macCatalyst(.v16)],
products: [
.library(name: "FireSideApp", type: .dynamic, targets: ["FireSide"]),
.library(name: "FireSideModel", targets: ["FireSideModel"]),
],
dependencies: [
.package(url: "https://source.skip.tools/skip.git", from: "1.0.0"),
.package(url: "https://source.skip.tools/skip-ui.git", from: "1.0.0"),
.package(url: "https://source.skip.tools/skip-foundation.git", from: "1.0.0"),
.package(url: "https://source.skip.tools/skip-model.git", from: "1.0.0"),
.package(url: "https://source.skip.tools/skip-firebase.git", "0.0.0"..<"2.0.0")
],
targets: [
.target(name: "FireSide", dependencies: [
"FireSideModel",
.product(name: "SkipUI", package: "skip-ui"),
.product(name: "SkipFirebaseMessaging", package: "skip-firebase")
], resources: [.process("Resources")], plugins: [.plugin(name: "skipstone", package: "skip")]),
.testTarget(name: "FireSideTests", dependencies: [
"FireSide",
.product(name: "SkipTest", package: "skip")
], resources: [.process("Resources")], plugins: [.plugin(name: "skipstone", package: "skip")]),
.target(name: "FireSideModel", dependencies: [
.product(name: "SkipFoundation", package: "skip-foundation"),
.product(name: "SkipModel", package: "skip-model"),
.product(name: "SkipFirebaseFirestore", package: "skip-firebase"),
.product(name: "SkipFirebaseAuth", package: "skip-firebase")
], resources: [.process("Resources")], plugins: [.plugin(name: "skipstone", package: "skip")]),
.testTarget(name: "FireSideModelTests", dependencies: [
"FireSideModel",
.product(name: "SkipTest", package: "skip")
], resources: [.process("Resources")], plugins: [.plugin(name: "skipstone", package: "skip")]),
]
)
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.plist
file should be placed in theDarwin/
folder of the Skip project - The
google-services.json
file 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.1" apply true
}
For concrete examples, see the FireSide Sample project. {: class="callout info"}
Once Firebase has been added to your project, you need to configure the FirebaseApp
on app startup. For iOS, this is typically done by setting an app delegate in your Darwin/Sources/AppMain.swift
file. Here is a snippet from the FireSide sample app:
import FirebaseCore
...
@main struct AppMain: App, FireSideApp {
@UIApplicationDelegateAdaptor(FireSideAppDelegate.self) var appDelegate
}
class FireSideAppDelegate : NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
...
return true
}
}
And for Android, configure FirebaseApp
in your Application.onCreate
function in Android/app/src/main/kotlin/.../Main.kt
:
import skip.firebase.core.FirebaseApp
...
open class AndroidAppMain: Application {
constructor() {
}
override fun onCreate() {
super.onCreate()
ProcessInfo.launch(applicationContext)
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 SKIP
import SkipFirebaseFirestore
#else
import 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 { ... }
...
}
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
minSdk
version 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 yourminSdk
will allow you to decide when to request notification permissions. To do so, edit yourAndroid/app/build.gradle.kts
file and change theminSdk
value 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
UNUserNotificationCenterDelegate
API, as well as the Firebase iOS SDK'sMessagingDelegate
. Here is an example delegate implementation that works across both platforms:import SwiftUI #if SKIP import SkipFirebaseMessaging #else import FirebaseMessaging #endif public class NotificationDelegate : NSObject, UNUserNotificationCenterDelegate, MessagingDelegate { 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)") } } } @MainActor 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] } @MainActor public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { let content = response.notification.request.content logger.info("didReceiveNotification: \(content.title): \(content.body) \(content.userInfo)") } 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 FireSide sample app:
// Darwin/Sources/FireSideAppMain.swift import FirebaseCore import FirebaseMessaging import FireSide ... @main struct AppMain: App, FireSideApp { @UIApplicationDelegateAdaptor(FireSideAppDelegate.self) var appDelegate } class FireSideAppDelegate : NSObject, UIApplicationDelegate { let notificationsDelegate = NotificationDelegate() // Defined in FireSideApp.swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { FirebaseApp.configure() UNUserNotificationCenter.current().delegate = notificationsDelegate Messaging.messaging().delegate = notificationsDelegate // Ask for permissions at a time appropriate for your app notificationsDelegate.requestPermission() application.registerForRemoteNotifications() return true } }
// Android/app/src/main/kotlin/.../Main.kt import skip.firebase.core.FirebaseApp import skip.firebase.messaging.Messaging ... internal val notificationsDelegate = NotificationDelegate() // Defined in FireSideApp.swift open class AndroidAppMain: Application { constructor() { } override fun onCreate() { super.onCreate() ProcessInfo.launch(applicationContext) FirebaseApp.configure() UNUserNotificationCenter.current().delegate = notificationsDelegate Messaging.messaging().delegate = notificationsDelegate } } open class MainActivity: AppCompatActivity { constructor() { } override fun onCreate(savedInstanceState: android.os.Bundle?) { super.onCreate(savedInstanceState) UIApplication.launch(this) // <-- Add this too if not present enableEdgeToEdge() setContent { val saveableStateHolder = rememberSaveableStateHolder() saveableStateHolder.SaveableStateProvider(true) { PresentationRootView(ComposeContext()) } } Messaging.messaging().onActivityCreated(this) // Ask for permissions at a time appropriate for your app notificationsDelegate.requestPermission() ... } ... }
Note that the call to
UIApplication.launch
inMainActivity.onCreate
may not be present if you have an older Skip project. Be sure to add it if it is not already there. -
See Firebase's iOS instructions and Android instructions for additional details and options, including how to send test messages to your apps!
The Fireside Sample project is a great reference for seeing a complete, working app using Firebase push notifications. {: class="callout info"}
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
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.