Usage – CocoaPods
1. Prerequisites
- Xcode 14 or later
- Swift 5.7+ or Objective-C project
- CocoaPods 1.11+ installed
gem install cocoapods - Firebase project with Cloud Messaging enabled - Required for push notifications
- Firebase Messaging SDK integrated in your app - You can use CocoaPods, SPM, or manual integration for Firebase
2. CocoaPods Setup
2.1 Create or Update Podfile
In your project root, create or update the
Podfileinstall! 'cocoapods', :disable_input_output_paths => true
source 'https://github.com/theaimegroup/BubblSpecs.git' (UPDATE with latest BUBBL URL)
source 'https://cdn.cocoapods.org/'
platform :ios, '15.0'
use_frameworks! :linkage => :static # ← static linkage
target 'YourApp' do
# Firebase Messaging - REQUIRED for Bubbl SDK (if using CocoaPods for Firebase)
pod 'Firebase/Messaging' # Optional: only if you choose CocoaPods for Firebase
# Bubbl SDK - THIS is what this guide covers
pod 'BubblPlugin', '2.1' # Current stable version
end
post_install do |installer|
# 1️⃣ Disable the macOS 14+ script sandbox
require 'find'
Find.find('Pods/Target Support Files') do |path|
next unless path.end_with?('.sh')
system('xattr', '-d', 'com.apple.quarantine', path) rescue nil
system('xattr', '-d', 'com.apple.provenance', path) rescue nil
system('chmod', '+x', path)
end
# 2️⃣ Delete unnecessary "[CP] ..." shell-script phases
installer.pods_project.targets.each do |t|
t.shell_script_build_phases
.select { |p| p.name&.start_with?('[CP]') }
.each { |p| t.build_phases.delete(p) }
end
end'15.0'Run
pod install or pod install --repo-update and open the newly created YourApp.xcworkspace from now on.2.2 Embed & Signing
1. Select your app’s target → “Frameworks, Libraries & Embedded Content.”
2. Click the “+” and choose BubblPlugin.xcframework.
3. Under the “Embed” column, select Embed & Sign.
3. Info.plist Permissions
Add the following keys to your
Info.plist to explain why your app needs location and notification permissions:<key>NSLocationWhenInUseUsageDescription</key>
<string>We use your location to trigger timely, relevant notifications.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Allow us to send notifications even when the app is in the background, so you never miss important alerts.</string>
4. Signing & Capabilities
- Background Modes → Location updates
- Push Notifications
5. Firebase Setup
Download
GoogleService-Info.plist from the Firebase console and add it to your app target.Then configure Firebase in your
AppDelegate.swift (or .m for Objective-C):import FirebaseCore
@main
struct YourApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene { WindowGroup { ContentView() } }
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
FirebaseApp.configure()
return true
}
}6. Bubbl Initialization
In your
AppDelegateconfigure Firebase, set up token handling, and initialize Bubbl:import FirebaseCore
import FirebaseMessaging
import Bubbl // Note: Import 'Bubbl', not 'BubblPlugin'
import UserNotifications
class AppDelegate: NSObject, UIApplicationDelegate, BubblPluginDelegate, MessagingDelegate {
private var fcmTokenReceived = false
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// 1️⃣ Configure Firebase FIRST (required for push notifications)
FirebaseApp.configure()
// 2️⃣ Set up Firebase Messaging delegate
Messaging.messaging().delegate = self
// 3️⃣ Request push notification permission
UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
if granted {
DispatchQueue.main.async {
application.registerForRemoteNotifications()
}
}
}
// Note: Bubbl SDK will be initialized after receiving FCM token
return true
}
// MARK: - Firebase Messaging Delegate
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
guard let fcmToken = fcmToken else { return }
// Forward FCM token to Bubbl SDK
BubblPlugin.updateFCMToken(fcmToken)
// Now initialize Bubbl SDK
if !fcmTokenReceived {
fcmTokenReceived = true
initializeBubbl()
}
}
private func initializeBubbl() {
BubblPlugin.shared.start(
apiKey: "YOUR_API_KEY",
env: .staging, // .production / .development
segmentations: ["beta_users", "vip"], // Optional: pass [] or omit for no segments
delegate: self
)
}
// MARK: - BubblPluginDelegate callbacks
func bubblPlugin(_ plugin: BubblPlugin, didAuthenticate deviceID: String, bubblID: String) {
print("✅ Device authenticated: (deviceID)")
}
func bubblPlugin(_ plugin: BubblPlugin, didFailWith error: Error) {
print("❌ Authentication failed: (error.localizedDescription)")
}
}6. API Reference
func start(
apiKey: String,
env: Config.Environment = .staging, // .development | .staging | .production
segmentations: [String] = [], // audience tags (empty by default)
delegate: BubblPluginDelegate? = nil
)What it does: Boots every SDK subsystem—device registration, geofence polling, live GPS, push-routing, analytics.
When to call: Once, as early as possible (e.g. in AppDelegate.application(_:didFinishLaunchingWithOptions:)).
// AppDelegate.swift
import FirebaseCore
import Bubbl
func application(_ app: UIApplication,
didFinishLaunchingWithOptions opts: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure() // Required for push notifications
// Note: Call BubblPlugin.shared.start() after receiving FCM token
// See complete example above in section 6
return true
}@objc public static func updateFCMToken(_ token: String)What it does: Forwards FCM token to the SDK for Firebase Cloud Messaging.
When to call: In your FCM delegate when FCM token is received.
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
if let token = fcmToken {
BubblPlugin.updateFCMToken(token)
}
}// Swift
public func updateSegments(
segmentations: [String],
completion: ((Result<Bool, Error>) -> Void)? = nil
)
// Objective-C
- (void)updateSegments:(NSArray *)segmentations
completion:(void (^)(BOOL success, NSError *error))completionWhat it does: Updates device segmentation tags after initialization. This allows you to dynamically change which campaigns/notifications the device receives based on user behavior or state changes.
When to call: After user login, subscription changes, preference updates, or any event that changes user segmentation.
Important: SDK must be initialized with start() before calling this method.
Swift Example:
// Update segments when user subscribes to premium
func userDidSubscribeToPremium() {
BubblPlugin.shared.updateSegments(segmentations: ["premium", "subscribed"]) { result in
switch result {
case .success:
print("✅ Segments updated successfully")
case .failure(let error):
print("❌ Failed to update segments: (error.localizedDescription)")
}
}
}
// Update segments based on user location region
func userDidChangeRegion(to region: String) {
let newSegments = ["region_(region)", "active_user"]
BubblPlugin.shared.updateSegments(segmentations: newSegments) { result in
if case .success = result {
print("User is now targeting region: (region)")
}
}
}Objective-C Example:
// Update segments when user subscribes to premium
- (void)userDidSubscribeToPremium {
NSArray *newSegments = @[@"premium", @"subscribed"];
[BubblPlugin.shared updateSegments:newSegments completion:^(BOOL success, NSError *error) {
if (success) {
NSLog(@"✅ Segments updated successfully");
} else {
NSLog(@"❌ Failed to update segments: %@", error.localizedDescription);
}
}];
}
// Update segments based on user preferences
- (void)updateUserSegments {
NSArray *segments = @[@"newsletter_subscriber", @"loyalty_member"];
[BubblPlugin.shared updateSegments:segments completion:nil]; // No completion handler needed
}Common Use Cases:
- User Authentication: Update segments when user logs in/out
- Subscription Changes: Premium, free, trial user segments
- Location/Region: Geographic targeting based on user location
- Preferences: Interest-based segments (sports, news, etc.)
- Behavior: Active users, power users, new users
- Demographics: Age groups, language preferences
func refetchGeofence()What it does: Manually triggers a geofence fetch from the backend. This is useful for refreshing geofences on-demand without waiting for the automatic polling interval (5 minutes).
When to call:
- After location permission is granted
- When user enters a new region or city
- After segments are updated (to get relevant geofences)
- On app launch if geofences haven't loaded yet
Swift Example:
// Manually trigger geofence refresh
BubblPlugin.shared.refetchGeofence()
// Common use case: After location permission granted
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
if manager.authorizationStatus == .authorizedWhenInUse ||
manager.authorizationStatus == .authorizedAlways {
// Refresh geofences now that we have location permission
BubblPlugin.shared.refetchGeofence()
}
}Objective-C Example:
// Manually trigger geofence refresh
[BubblPlugin.shared refetchGeofenceObjC];
// Common use case: After location permission granted
- (void)locationManagerDidChangeAuthorization:(CLLocationManager *)manager {
CLAuthorizationStatus status = manager.authorizationStatus;
if (status == kCLAuthorizationStatusAuthorizedWhenInUse ||
status == kCLAuthorizationStatusAuthorizedAlways) {
// Refresh geofences now that we have location permission
[BubblPlugin.shared refetchGeofenceObjC];
}
}func getPrivacyText() -> String
func refreshPrivacyText(completion: ((Result<String, Error>) -> Void)?)What they do:
getPrivacyText()- Returns cached privacy text without network callrefreshPrivacyText()- Fetches latest configuration from backend
When to use: Display privacy policy text in settings, consent screens, or onboarding flows. The text is configured in your Bubbl dashboard.
Swift Example:
// Get cached privacy text
let privacyText = BubblPlugin.shared.getPrivacyText()
print("Privacy: \(privacyText)")
// Refresh from backend
BubblPlugin.shared.refreshPrivacyText { result in
switch result {
case .success(let text):
print("Updated privacy text: \(text)")
// Update UI with new text
self.privacyLabel.text = text
case .failure(let error):
print("Failed to refresh: \(error)")
}
}
// Access full configuration (advanced)
if let config = BubblPlugin.shared.getCurrentConfiguration() {
print("Notifications count: \(config.notificationsCount)")
print("Days count: \(config.daysCount)")
print("Battery count: \(config.batteryCount)")
print("Privacy text: \(config.privacyText)")
}Objective-C Example:
// Get cached privacy text
NSString *privacyText = [BubblPlugin.shared getPrivacyTextObjC];
NSLog(@"Privacy: %@", privacyText);
// Refresh from backend
[BubblPlugin.shared refreshPrivacyText:^(NSString *text, NSError *error) {
if (error) {
NSLog(@"Failed to refresh: %@", error);
} else {
NSLog(@"Updated privacy text: %@", text);
// Update UI with new text
self.privacyLabel.text = text;
}
}];refreshPrivacyText() only when you need to show the latest version to users.// BubblPluginDelegate
func bubblPlugin(_ plugin: BubblPlugin,
didAuthenticate deviceID: String,
bubblID: String)
func bubblPlugin(_ plugin: BubblPlugin,
didFailWith error: Error)What they do: Authentication callbacks when the SDK successfully registers the device or encounters an error. Use these for initialization confirmation and error handling.
Note: For geofence events, use the GeofenceService or ForegroundGeofenceMonitor classes which provide real-time location-based callbacks.
func requestLocationWhenInUse()What it does: Requests location permission for when the app is in use.
When to call: When you need location access during app usage.
func requestLocationAlways()What it does: Requests location permission for background location access.
When to call: When you need location access in the background for geofencing.
func requestPushPermission()What it does: Requests push notification permission.
When to call: When you want to send push notifications to the user.
// Location updates you can observe
func locationManager(_ mgr: CLLocationManager,
didUpdateLocations locs: [CLLocation])
func locationManagerDidChangeAuthorization(_ mgr: CLLocationManager)Why expose them: If you want your ownCLLocationManager delegate tap-through without breaking Bubbl’s internal handling, forward these calls.
public static let shared: BubblPluginGlobal singleton – used only for lightweight helpers; the main surface of the SDK is static.
public var locationAuthorizationStatus: CLAuthorizationStatus – current location permission statuspublic var pushAuthorizationStatus: UNAuthorizationStatus – current push notification permission statuspublic var locationAuthorizationStatusRaw: Int – location status as integer (for KVC)public var pushAuthorizationStatusRaw: Int – push status as integer (for KVC)Usage Examples
Swift - Checking Permission Status:
// Check current location permission status
let locationStatus = BubblPlugin.shared.locationAuthorizationStatus
switch locationStatus {
case .notDetermined:
print("Location permission not yet requested")
BubblPlugin.shared.requestLocationWhenInUse()
case .denied, .restricted:
print("Location permission denied - show settings alert")
case .authorizedWhenInUse:
print("Location permission granted for when in use")
// Request always permission for geofencing
BubblPlugin.shared.requestLocationAlways()
case .authorizedAlways:
print("Location permission granted for always - ready for geofencing")
@unknown default:
print("Unknown location permission status")
}
// Check push notification permission status
let pushStatus = BubblPlugin.shared.pushAuthorizationStatus
switch pushStatus {
case .notDetermined:
print("Push permission not yet requested")
BubblPlugin.shared.requestPushPermission()
case .denied:
print("Push permission denied - show settings alert")
case .authorized:
print("Push notifications enabled")
case .provisional:
print("Push notifications provisionally authorized")
@unknown default:
print("Unknown push permission status")
}Objective-C - Using Raw Integer Values:
// Check location permission using raw integer values (useful for KVC)
NSInteger locationStatusRaw = [BubblPlugin shared].locationAuthorizationStatusRaw;
if (locationStatusRaw == 0) {
NSLog(@"Location permission not determined");
} else if (locationStatusRaw == 1 || locationStatusRaw == 2) {
NSLog(@"Location permission denied or restricted");
} else if (locationStatusRaw == 3) {
NSLog(@"Location authorized when in use");
} else if (locationStatusRaw == 4) {
NSLog(@"Location authorized always - ready for geofencing");
}
// Check push permission using raw integer values
NSInteger pushStatusRaw = [BubblPlugin shared].pushAuthorizationStatusRaw;
if (pushStatusRaw == 0) {
NSLog(@"Push permission not determined");
} else if (pushStatusRaw == 1) {
NSLog(@"Push permission denied");
} else if (pushStatusRaw == 2) {
NSLog(@"Push notifications authorized");
}SwiftUI - Reactive Updates:
import SwiftUI
import Combine
import Bubbl
struct PermissionStatusView: View {
@State private var locationStatus: CLAuthorizationStatus = .notDetermined
@State private var pushStatus: UNAuthorizationStatus = .notDetermined
private var cancellables = Set<AnyCancellable>()
var body: some View {
VStack(spacing: 16) {
// Location Status
HStack {
Text("Location:")
Spacer()
Text(locationStatusText)
.foregroundColor(locationStatusColor)
}
// Push Status
HStack {
Text("Push Notifications:")
Spacer()
Text(pushStatusText)
.foregroundColor(pushStatusColor)
}
// Action buttons
if locationStatus != .authorizedAlways {
Button("Request Location Permission") {
BubblPlugin.shared.requestLocationAlways()
}
}
if pushStatus != .authorized {
Button("Request Push Permission") {
BubblPlugin.shared.requestPushPermission()
}
}
}
.onAppear {
updateStatus()
setupStatusObservers()
}
}
private func updateStatus() {
locationStatus = BubblPlugin.shared.locationAuthorizationStatus
pushStatus = BubblPlugin.shared.pushAuthorizationStatus
}
private func setupStatusObservers() {
// Subscribe to permission changes
BubblPlugin.locationAuthorizationPublisher
.sink { status in
locationStatus = status
}
.store(in: &cancellables)
BubblPlugin.pushAuthorizationPublisher
.sink { status in
pushStatus = status
}
.store(in: &cancellables)
}
private var locationStatusText: String {
switch locationStatus {
case .notDetermined: return "Not Asked"
case .denied, .restricted: return "Denied"
case .authorizedWhenInUse: return "When In Use"
case .authorizedAlways: return "Always"
@unknown default: return "Unknown"
}
}
private var pushStatusText: String {
switch pushStatus {
case .notDetermined: return "Not Asked"
case .denied: return "Denied"
case .authorized: return "Authorized"
case .provisional: return "Provisional"
@unknown default: return "Unknown"
}
}
private var locationStatusColor: Color {
switch locationStatus {
case .authorizedAlways: return .green
case .authorizedWhenInUse: return .orange
case .denied, .restricted: return .red
default: return .gray
}
}
private var pushStatusColor: Color {
switch pushStatus {
case .authorized: return .green
case .denied: return .red
default: return .gray
}
}
}3.10 SDK Capabilities & Best Practices
Automatic LoggingWhat it does: The SDK automatically logs important events to the console.
Why use it: No manual logging needed - the SDK handles all debugging information.
Where to see: Check Xcode console for logs prefixed with [Bubbl] or similar.
// SDK automatically logs:
// - Device registration
// - Geofence events
// - Notification delivery
// - API calls
// - Error conditionsCombine Publishers (Swift Only)What: Reactive streams for location and notification authorization status.
Why: Modern Swift integration with reactive programming patterns.
When: For SwiftUI apps or reactive architectures.
// Subscribe to location authorization changes
BubblPlugin.locationAuthorizationPublisher
.sink { status in
print("Location status: (status)")
}
.store(in: &cancellables)
// Subscribe to push authorization changes
BubblPlugin.pushAuthorizationPublisher
.sink { status in
print("Push status: (status)")
}
.store(in: &cancellables)Geofence MonitoringWhat: Automatic geofence detection and notification triggering.
Why: Location-based notifications work out of the box.
How: SDK automatically monitors geofences when location permissions are granted.
// SDK automatically:
// - Monitors geofences in background
// - Triggers notifications on entry/exit
// - Handles location permission changes
// - Manages battery optimizationFirebase IntegrationWhat: Seamless Firebase Cloud Messaging integration.
Why: Reliable push notification delivery.
How: Forward FCM tokens to the SDK for optimal delivery.
// Forward FCM token to SDK
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
if let token = fcmToken {
BubblPlugin.updateFCMToken(token)
}
}3.11 Delegate & System Callbacks
// MARK: – BubblPluginDelegateImplement on any class (often your AppDelegate or a dedicatedGeoObserver) to receive foreground polygon events:
extension AppDelegate: BubblPluginDelegate {
func bubblPlugin(
_ plugin: BubblPlugin,
didAuthenticate deviceID: String,
bubblID: String
) {
// SDK successfully registered device
print("Device authenticated: (deviceID)")
}
func bubblPlugin(
_ plugin: BubblPlugin,
didFailWith error: Error
) {
// SDK registration failed
print("Authentication failed: (error.localizedDescription)")
}
}// MARK: – UNUserNotificationCenterDelegateAlready wired inside NotificationManager, but expose it here in case you subclass further:
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completion: @escaping (UNNotificationPresentationOptions) -> Void
) { … }
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completion: @escaping () -> Void
) { … }// MARK: – CLLocationManagerDelegateExposed via LocationManager.shared if you want raw GPS hooks:
func locationManager(
_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]
)
func locationManager(
_ manager: CLLocationManager,
didFailWithError error: Error
)
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager)AppDelegate.• For SwiftUI, create small
ObservableObjects (e.g. GeoObserver,PushObserver) and inject them via .environmentObject.7. BubblNotificationDetails API Reference
When a user taps a Bubbl notification, you receive a BubblNotificationDetails object with the following properties:
headline: String
The main title/headline of the notification
body: String
The body text content of the notification
mediaType: String?
The type of media attached (e.g., "image", "video", or nil for text-only)
mediaURL: String?
URL to the media file (image, video, etc.) if present
ctaLabel: String?
Label text for the call-to-action button (if present)
ctaURL: String?
URL to open when the call-to-action button is tapped
notifID: Int
Unique identifier for this notification (for analytics tracking)
locationID: Int
ID of the location that triggered this notification
8. Handling Tapped Notifications
The SDK provides NotificationManager to handle notification presentation and user interactions. For SwiftUI apps, you can use the published latest property to show notification details:
import SwiftUI
import Bubbl
struct ContentView: View {
@ObservedObject private var notif = NotificationManager.shared
var body: some View {
NavigationView {
// Your main UI here
}
// Present modal when notification is tapped
.sheet(item: $notif.latest) { details in
NotificationModalView(details: details)
}
}
}NotificationModalView Implementation
Here's a complete implementation of NotificationModalView for displaying notification content:
import SwiftUI
import Bubbl
struct NotificationModalView: View {
let details: BubblNotificationDetails
@Environment(.dismiss) private var dismiss
var body: some View {
NavigationView {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
// Headline
Text(details.headline)
.font(.title2)
.fontWeight(.bold)
// Body
Text(details.body)
.font(.body)
.foregroundColor(.secondary)
// Media content (if any)
if let mediaURL = details.mediaURL {
AsyncImage(url: URL(string: mediaURL)) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(12)
} placeholder: {
RoundedRectangle(cornerRadius: 12)
.fill(Color.gray.opacity(0.2))
.frame(height: 200)
.overlay(
ProgressView()
)
}
}
// Additional notification info
VStack(alignment: .leading, spacing: 8) {
Text("Notification Details")
.font(.headline)
HStack {
Text("Type:")
.fontWeight(.medium)
Spacer()
Text(details.mediaType ?? "text")
.foregroundColor(.secondary)
}
HStack {
Text("Notification ID:")
.fontWeight(.medium)
Spacer()
Text("(details.notifID)")
.foregroundColor(.secondary)
}
HStack {
Text("Location ID:")
.fontWeight(.medium)
Spacer()
Text("(details.locationID)")
.foregroundColor(.secondary)
}
}
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
// Call-to-action button
if let ctaLabel = details.ctaLabel, let ctaURL = details.ctaURL {
Link(destination: URL(string: ctaURL)!) {
HStack {
Text(ctaLabel)
Image(systemName: "arrow.up.right.square")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
}
}
.padding()
}
.navigationTitle("Notification")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
dismiss()
}
}
}
}
}
}For UIKit apps, observe the notification manager's changes:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Observe notification taps
NotificationCenter.default.addObserver(
self,
selector: #selector(handleNotificationTap),
name: NSNotification.Name("BubblNotificationTapped"),
object: nil
)
}
@objc func handleNotificationTap() {
if let details = NotificationManager.shared.latest {
// Present your notification UI
presentNotificationDetails(details)
}
}
}9. Common Problems & Solutions
- Framework not found at runtime: Ensure you embedded & signed and that the .xcframework appears under your app target’s “Embedded Content.”
- Push notifications never arrive:
- Confirm you called
registerForRemoteNotifications(). - Verify your Firebase Cloud Messaging setup.
- Confirm you called
- Geofences not triggering:
- Make sure “Location updates” is checked in Background Modes.
- Check your campaigns and locations in platform and their status.
- App crashes at startup: Look at the device logs for missing or mis-signed frameworks; check your “Embedded Content” settings again.
- Pod install fails: Run
and try again.pod repo update - Push notifications not arriving: Confirm that
is being forwarded correctly and check your FCM setup.deviceToken