Swift Notifications

One API. Three Platforms. Zero Native Setup.

A unified Flutter plugin for rich push and local notifications on Android, iOS, and macOS with no native code required.

🌟 Features

  • Unified API - Same code works on Android, iOS, and macOS
  • Rich Notifications - Support for images and action buttons
  • Zero Native Setup - No manifest edits, no plist configuration, no native code
  • Automatic Setup - Permissions, channels, categories handled automatically
  • Works Everywhere - Foreground, background, and terminated app states
  • Graceful Fallbacks - Falls back to text notifications if images fail

📦 Installation

Add this to your package's pubspec.yaml file:

dependencies:
  swift_notifications: ^0.0.1

Then run:

flutter pub get

🚀 Quick Start

1. Initialize the Plugin

import 'package:swift_notifications/swift_notifications.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  final notifications = SwiftNotifications();
  await notifications.initialize();
  await notifications.requestPermission();
  
  runApp(MyApp());
}

2. Show a Simple Notification

await notifications.showSimpleNotification(
  id: 'notification_1',
  title: 'Hello!',
  body: 'This is a simple notification',
);

3. Show a Rich Notification with Image

await notifications.showImageNotification(
  id: 'notification_2',
  title: 'Check this out!',
  body: 'This notification has an image',
  imageUrl: 'https://example.com/image.jpg',
);

4. Show a Notification with Buttons

await notifications.showNotification(
  NotificationRequest(
    id: 'notification_3',
    title: 'New Message',
    body: 'You have a new message',
    buttonsEnabled: true,
    actions: [
      NotificationAction(
        id: 'reply',
        title: 'Reply',
        payload: {'action': 'reply'},
      ),
      NotificationAction(
        id: 'delete',
        title: 'Delete',
        isDestructive: true,
        payload: {'action': 'delete'},
      ),
    ],
  ),
);

5. Handle Notification Responses

notifications.onNotificationResponse.listen((response) {
  print('Notification ID: ${response.notificationId}');
  print('Action ID: ${response.actionId}');
  print('Payload: ${response.payload}');
  
  if (response.actionId == 'reply') {
    // Handle reply action
  }
});

📚 API Reference

SwiftNotifications

Main class for managing notifications.

Methods

initialize()

Initialize the notification plugin. Call this once when your app starts.

await notifications.initialize();
requestPermission()

Request notification permissions from the user.

final status = await notifications.requestPermission();
// Returns: NotificationPermissionStatus.granted, .denied, etc.
checkPermission()

Check the current permission status without requesting.

final status = await notifications.checkPermission();
showNotification(NotificationRequest request)

Show a notification with full customization.

await notifications.showNotification(
  NotificationRequest(
    id: 'unique_id',
    title: 'Title',
    body: 'Body',
    // ... see NotificationRequest for all options
  ),
);
showSimpleNotification({required String id, required String title, required String body, Map<String, dynamic>? payload})

Convenience method for simple text notifications.

await notifications.showSimpleNotification(
  id: 'simple_1',
  title: 'Hello',
  body: 'World',
  payload: {'key': 'value'},
);
showImageNotification({required String id, required String title, required String body, required String imageUrl, Map<String, dynamic>? payload})

Convenience method for notifications with images.

await notifications.showImageNotification(
  id: 'image_1',
  title: 'Image',
  body: 'Notification',
  imageUrl: 'https://example.com/image.jpg',
);
cancelNotification(String notificationId)

Cancel a specific notification.

await notifications.cancelNotification('notification_1');
cancelAllNotifications()

Cancel all notifications.

await notifications.cancelAllNotifications();

Streams

onNotificationResponse

Stream of notification responses (taps and button clicks).

notifications.onNotificationResponse.listen((response) {
  // Handle notification response
});

NotificationRequest

Model for notification configuration.

NotificationRequest(
  id: 'unique_id',                    // Required: Unique identifier
  title: 'Title',                     // Required: Notification title
  body: 'Body',                       // Required: Notification body
  image: NotificationImage(...),      // Optional: Image to display
  imageEnabled: true,                 // Optional: Enable image feature
  actions: [NotificationAction(...)], // Optional: Action buttons
  buttonsEnabled: true,               // Optional: Enable buttons feature
  categoryId: 'custom_category',      // Optional: Category identifier
  payload: {'key': 'value'},          // Optional: Custom payload
  sound: 'default',                   // Optional: Sound name
  badge: 1,                           // Optional: Badge number (iOS/macOS)
  priority: NotificationPriority.high, // Optional: Priority (Android)
  showWhenForeground: true,           // Optional: Show when app is in foreground
)

NotificationAction

Model for notification action buttons.

NotificationAction(
  id: 'action_id',                    // Required: Unique action identifier
  title: 'Action Title',              // Required: Button label
  payload: {'key': 'value'},         // Optional: Action payload
  requiresAuthentication: false,      // Optional: Require authentication
  isDestructive: false,               // Optional: Mark as destructive (red on iOS)
)

NotificationImage

Model for notification images.

NotificationImage(
  url: 'https://example.com/image.jpg', // Required: Image URL
  localPath: '/path/to/image.jpg',     // Optional: Local file path
  downloadFromUrl: true,                // Optional: Download from URL
)

NotificationResponse

Model for notification responses.

NotificationResponse(
  notificationId: 'notification_1',     // The notification that was tapped
  actionId: 'reply',                   // The action button clicked (null if body tapped)
  payload: {'key': 'value'},          // Notification payload
  actionPayload: {'action': 'reply'}, // Action payload (if action clicked)
)

🎯 Platform Support

Feature Android iOS macOS
Basic Notifications
Images
Buttons
Categories
Permissions
Foreground Display
Background Display
Terminated Display

🔧 Platform-Specific Notes

Android

  • Minimum SDK: 24 (Android 7.0)
  • Permissions: Automatically handled. POST_NOTIFICATIONS permission is declared in the plugin.
  • Channels: Automatically created. Default channel ID: swift_notifications_default
  • Images: Downloaded and displayed using BigPictureStyle
  • Buttons: Implemented using NotificationCompat actions

iOS

  • Minimum Version: iOS 10.0
  • Permissions: Requested automatically via UNUserNotificationCenter
  • Categories: Automatically created and registered
  • Images: Downloaded and attached as UNNotificationAttachment
  • Buttons: Implemented using UNNotificationAction

macOS

  • Minimum Version: macOS 10.14
  • Permissions: Requested automatically via UNUserNotificationCenter
  • Categories: Automatically created and registered
  • Images: Downloaded and attached as UNNotificationAttachment
  • Buttons: Implemented using UNNotificationAction

📡 Remote Push Notifications

Android (Firebase Cloud Messaging)

To use remote push notifications with Firebase, you need to create your own Firebase Messaging Service. The plugin provides a helper class NotificationBuilder to make this easy:

  1. Add Firebase to your app:

    dependencies:
      firebase_core: ^2.24.0
      firebase_messaging: ^14.7.0
    
  2. Add Firebase Messaging dependency in android/app/build.gradle:

    dependencies {
        implementation 'com.google.firebase:firebase-messaging:23.4.0'
    }
    
  3. Create your Firebase Messaging Service using the helper class:

    Create android/app/src/main/kotlin/com/yourapp/YourFirebaseMessagingService.kt:

    package com.yourapp
       
    import android.util.Log
    import com.google.firebase.messaging.FirebaseMessagingService
    import com.google.firebase.messaging.RemoteMessage
    import com.swiftflutter.swift_notifications.NotificationBuilder
       
    class YourFirebaseMessagingService : FirebaseMessagingService() {
        override fun onMessageReceived(remoteMessage: RemoteMessage) {
            Log.d("FCM", "Received message: ${remoteMessage.messageId}")
               
            val data = remoteMessage.data
            val notification = remoteMessage.notification
               
            // Extract notification data from Firebase payload
            val id = data["id"] ?: remoteMessage.messageId ?: System.currentTimeMillis().toString()
            val title = data["title"] ?: notification?.title ?: "Notification"
            val body = data["body"] ?: notification?.body ?: ""
            val imageUrl = data["image"]
            val imageEnabled = data["imageEnabled"]?.toBoolean() ?: !imageUrl.isNullOrEmpty()
            val buttonsJson = data["buttons"]
            val buttonsEnabled = data["buttonsEnabled"]?.toBoolean() ?: false
            val categoryId = data["categoryId"]
            val payload = data["payload"]
            val priority = data["priority"] ?: "defaultPriority"
               
            // Use the plugin's helper to show rich notification
            NotificationBuilder.showRichNotification(
                context = applicationContext,
                id = id,
                title = title,
                body = body,
                imageUrl = imageUrl,
                imageEnabled = imageEnabled,
                buttonsJson = buttonsJson,
                buttonsEnabled = buttonsEnabled,
                categoryId = categoryId,
                payload = payload,
                priority = priority
            )
        }
           
        override fun onNewToken(token: String) {
            Log.d("FCM", "New token: $token")
            // Send token to your server if needed
        }
    }
    
  4. Register your service in android/app/src/main/AndroidManifest.xml:

    <service
        android:name=".YourFirebaseMessagingService"
        android:exported="false">
        <intent-filter>
            <action android:name="com.google.firebase.MESSAGING_EVENT" />
        </intent-filter>
    </service>
    
  5. Send notifications using the unified payload format in your Firebase data payload:

    {
      "id": "notification_123",
      "title": "New Message",
      "body": "You have a new message",
      "image": "https://example.com/image.jpg",
      "imageEnabled": "true",
      "buttons": "[{\"id\":\"reply\",\"title\":\"Reply\"},{\"id\":\"delete\",\"title\":\"Delete\",\"isDestructive\":true}]",
      "buttonsEnabled": "true",
      "payload": "{\"type\":\"message\",\"userId\":\"123\"}",
      "priority": "high"
    }
    

That's it! The NotificationBuilder helper class handles:

  • ✅ Creating notification channels
  • ✅ Downloading and attaching images
  • ✅ Creating action buttons
  • ✅ Setting up tap and action receivers
  • ✅ All the rich notification features
  • Works in background - Firebase service runs in background when app is closed

Important Notes:

  • Background Support: Yes! NotificationBuilder works when notifications are received in the background. The Firebase Messaging Service runs automatically when a push notification arrives, even if the app is closed.
  • Foreground Support: Also works when app is in foreground.
  • Terminated App: Works when app is completely closed (killed).

Your Firebase service just needs to extract the data and call NotificationBuilder.showRichNotification().

iOS (APNs with Notification Service Extension)

For iOS rich push notifications with images, you need to set up a Notification Service Extension (NSE). The plugin provides a NotificationBuilder helper class for iOS as well.

Important Notes:

  • Background Support: Yes! Works when notifications are received in background
  • Foreground Support: Works when app is in foreground
  • Terminated App: Works when app is completely closed
  • ⚠️ NSE Required: For images in remote push, you MUST use a Notification Service Extension (NSE). This is an iOS requirement.

This is required if you want images in remote push notifications.

Step 1: Create Notification Service Extension Target

  1. Open your project in Xcode
  2. Go to File → New → Target
  3. Select Notification Service Extension
  4. Name it (e.g., "NotificationServiceExtension")
  5. Click Finish and Activate the scheme

Step 2: Update Extension Code

Replace the content of NotificationServiceExtension/NotificationService.swift:

import UserNotifications
import swift_notifications

class NotificationService: UNNotificationServiceExtension {
    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?

    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
        
        guard let bestAttemptContent = bestAttemptContent else {
            contentHandler(request.content)
            return
        }
        
        // Extract data from notification payload
        let userInfo = bestAttemptContent.userInfo
        let imageUrl = userInfo["image"] as? String
        let imageEnabled = (userInfo["imageEnabled"] as? String) == "true" || imageUrl != nil
        let buttonsJson = userInfo["buttons"] as? String
        let buttonsEnabled = (userInfo["buttonsEnabled"] as? String) == "true"
        let categoryId = userInfo["categoryId"] as? String
        
        // Setup category with buttons if needed
        if buttonsEnabled {
            NotificationBuilder.setupCategory(
                categoryId: categoryId,
                buttonsJson: buttonsJson,
                buttonsEnabled: buttonsEnabled
            )
            bestAttemptContent.categoryIdentifier = categoryId ?? "swift_notifications_default"
        }
        
        // Download and attach image if enabled
        if imageEnabled, let imageUrl = imageUrl, let url = URL(string: imageUrl) {
            NotificationBuilder.downloadAndAttachImage(url: url, content: bestAttemptContent) { success in
                if !success {
                    print("Warning: Failed to load image")
                }
                contentHandler(bestAttemptContent)
            }
        } else {
            contentHandler(bestAttemptContent)
        }
    }
    
    override func serviceExtensionTimeWillExpire() {
        if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }
}

Step 3: Configure App Group (Optional, for sharing data)

If you need to share data between app and extension:

  1. Enable App Groups in both app and extension targets
  2. Use the same group identifier (e.g., group.com.yourapp.notifications)

Step 4: Send Remote Push with Image

When sending remote push from your server, include the image URL in the payload:

{
  "aps": {
    "alert": {
      "title": "New Message",
      "body": "You have a new message"
    },
    "mutable-content": 1
  },
  "image": "https://example.com/image.jpg",
  "id": "notification_123",
  "payload": {
    "type": "message",
    "userId": "123"
  }
}

Important: The mutable-content: 1 key is required for NSE to process the notification.

Option 2: Using AppDelegate (For Basic Notifications Without Images)

If you don't need images in remote push, you can handle notifications in your AppDelegate:

import UserNotifications
import swift_notifications

extension AppDelegate: UNUserNotificationCenterDelegate {
    // Handle notification when app is in foreground
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        // Setup category with buttons if needed
        let userInfo = notification.request.content.userInfo
        let buttonsJson = userInfo["buttons"] as? String
        let buttonsEnabled = (userInfo["buttonsEnabled"] as? String) == "true"
        let categoryId = userInfo["categoryId"] as? String
        
        if buttonsEnabled {
            NotificationBuilder.setupCategory(
                categoryId: categoryId,
                buttonsJson: buttonsJson,
                buttonsEnabled: buttonsEnabled
            )
        }
        
        completionHandler([.banner, .sound, .badge])
    }
    
    // Handle notification tap
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                didReceive response: UNNotificationResponse,
                                withCompletionHandler completionHandler: @escaping () -> Void) {
        // Handle tap/action - events are automatically handled by the plugin
        completionHandler()
    }
}

Note: AppDelegate approach works for basic notifications, but for images in remote push, you MUST use NSE (Option 1).

macOS (APNs)

macOS remote push works similarly to iOS but doesn't require NSE. You can handle remote push in your AppDelegate.swift:

import UserNotifications

extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        // Handle notification when app is in foreground
        completionHandler([.banner, .sound, .badge])
    }
    
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                didReceive response: UNNotificationResponse,
                                withCompletionHandler completionHandler: @escaping () -> Void) {
        // Handle notification tap
        let userInfo = response.notification.request.content.userInfo
        // Process userInfo and route to appropriate screen
        completionHandler()
    }
}

Note: For macOS, images in remote push can be attached directly in the delegate methods without requiring an extension.

🧊 Cold-Start Event Handling

The plugin automatically handles notification taps and button clicks even when the app is terminated (cold start):

  • Events are stored in persistent storage when app is not running
  • Events are delivered when the app starts and Flutter is ready
  • Works seamlessly - no additional code required

Just listen to the onNotificationResponse stream as usual:

notifications.onNotificationResponse.listen((response) {
  // This will receive events even from cold start
  print('Notification: ${response.notificationId}');
  print('Action: ${response.actionId}');
});

🎯 Screen Routing with screen_launch_by_notfication

For advanced screen routing and navigation when notifications are tapped (especially from cold start), integrate with the screen_launch_by_notfication plugin.

Why Use screen_launch_by_notfication?

  • Automatic routing: Skip splash screens and route directly to notification-specific screens
  • Deep link support: Handle custom URL schemes and universal links
  • Cold start handling: Works when app is killed, in background, or foreground
  • Zero native setup: All native code handled automatically

Integration Example

import 'package:swift_notifications/swift_notifications.dart';
import 'package:screen_launch_by_notfication/screen_launch_by_notfication.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  final notifications = SwiftNotifications();
  await notifications.initialize();
  await notifications.requestPermission();
  
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SwiftFlutterMaterial(
      materialApp: MaterialApp(
        title: 'My App',
        routes: {
          '/home': (context) => HomeScreen(),
          '/message': (context) => MessageScreen(),
          '/order': (context) => OrderScreen(),
        },
      ),
      onNotificationLaunch: ({required isFromNotification, required payload}) {
        if (isFromNotification && payload.isNotEmpty) {
          // Route based on notification type
          if (payload['type'] == 'message') {
            return SwiftRouting(
              route: '/message',
              payload: {
                'messageId': payload['messageId'],
                'senderId': payload['senderId'],
              },
            );
          } else if (payload['type'] == 'order') {
            return SwiftRouting(
              route: '/order',
              payload: {
                'orderId': payload['orderId'],
              },
            );
          }
        }
        return null; // Use default route
      },
    );
  }
}

Handling Notification Button Actions

You can combine both plugins to handle button actions and route accordingly:

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final notifications = SwiftNotifications();
  
  @override
  void initState() {
    super.initState();
    
    // Listen to notification responses (button clicks, taps)
    notifications.onNotificationResponse.listen((response) {
      // Handle button actions
      if (response.actionId == 'view_order') {
        Navigator.pushNamed(context, '/order', arguments: {
          'orderId': response.payload?['orderId'],
        });
      } else if (response.actionId == 'reply') {
        Navigator.pushNamed(context, '/message', arguments: {
          'messageId': response.payload?['messageId'],
        });
      }
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return SwiftFlutterMaterial(
      materialApp: MaterialApp(
        // ... your app config
      ),
      onNotificationLaunch: ({required isFromNotification, required payload}) {
        // Handle cold start routing
        if (isFromNotification) {
          return SwiftRouting(
            route: '/notification',
            payload: payload,
          );
        }
        return null;
      },
    );
  }
}

This combination gives you:

  • Cold start routing via screen_launch_by_notfication
  • Button action handling via swift_notifications
  • Seamless navigation in all app states

🛡️ Error Handling

The plugin gracefully handles errors:

  • Image download fails: Falls back to text notification
  • Invalid actions: Notification shown without buttons
  • Permission denied: Methods return appropriate error status
  • Network offline: Image notifications fall back to text
  • Cold start events: Automatically stored and delivered when app starts

🐛 Debugging

Enable verbose logging to debug notification issues:

// Enable verbose logging
SwiftNotifications.enableVerboseLogging();

// Or during initialization
await notifications.initialize(verbose: true);

This will print detailed logs about:

  • Notification creation
  • Image downloads
  • Event handling
  • Cold-start event storage and retrieval
  • Permission status

📝 Examples

See the example/ directory for a complete working example demonstrating:

  • Simple notifications
  • Image notifications
  • Button notifications
  • Rich notifications (image + buttons)
  • Handling notification responses
  • Permission management
  • Cold-start event handling

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

📄 License

See the LICENSE file for details.

🙏 Acknowledgments

Built with ❤️ for the Flutter community.


One API. Three Platforms. Zero Native Setup.