Via Cà Matta 2 - Peschiera Borromeo (MI)
+39 02 00704272
info@synaptica.info

Delphi 11 – Firebase Cloud Messaging ( FCM ) – iOS app Push Notification

Digital solution partner

Delphi 11 – Firebase Cloud Messaging ( FCM ) – iOS app Push Notification

Goals:

  • Integrate Firebase push notifications (FCM) into an app built with Delphi for iOS without the help of third-party libraries.
  • Having the same base code in the application and in the servers for managing pushes in Delphi the same as that used for Android.

 

Test environment used:

  • VM Delphi 11 Alexandria Ent. Edition on Windows 10 pro 64GB Ram
  • Xcode 13.2.1 
  • Mac OS Menterey 12.0.1
  • iOS sdk iPhoneOS 14.5
  • Device test
    • iPad 10.2 myl92ty/a os. ver 14.3
    • iPhone 12 mini

Prerequisites:

  • IOS application operational on your devices in development
  • Firebase account and created project

Steps needed to associate notifications

  1. Configure the distribution of the app on the developer.apple.com portal and on https://appstoreconnect.apple.com/apps/ <appId>
  2. Add the management code on the Delphi application and import the firebase sdk for iOS
  3. Configure the project on firebase https://console.firebase.google.com/project/
  4. Download the “GoogleService-Info.plist” file from firebase, copy it to the project directory and insert it in the distribution list (deploy)
  5. Sending a test push message from the firebase console or your server

Step 1: Configure the distribution of the app on the developer.apple.com portal and on https://appstoreconnect.apple.com/apps/ <appId>

  • (apple portal) Configure the distribution of the app on the apple developer portal to receive pushes
    • create a certificate for push notifications
    • enable “Push Notifications” in the “Identifier” section and associate a certificate for the “Production SSL Certificate” push (mac keystore -> Keychain Access -> Certificate Assistant -> Request a certificate from a certification authority)
    • always in the “keys” section of your developers panel Create a key for pushes
    • (if not already done) create an app on “app Store Connect” (the “sku” code is your internal code (write us what you want, just remember it))
    • publish the app on the App Store Connect even without push support using the “Transporter” program

Step 2: Add the management code on the Delphi application and import the firebase sdk for iOS

  • (delphi) Download the FCM SDK package from getit and unzip it in the proposed directory or choose a custom path
  • (delphi) Create an “environment variable” in which to define the base path of Friebase SDK for iOS (in my case C: \ Users \ <myusername> \ Documents \ Embarcadero \ Studio \ 22.0 \ CatalogRepository \ FirebaseSDKforiOS- 6.28 \ Firebase) and which I called “Firebase_6_28
  • (delphi) Change the search path in the project options ( project –> options –> Delphi compiler –> search Path :
    • $(Firebase_6_28)\FirebaseAnalytics\nanopb.xcframework\ios-armv7_arm64\nanopb.framework;$(Firebase_6_28)\FirebaseAnalytics\GoogleUtilities.xcframework\ios-armv7_arm64\GoogleUtilities.framework;$(Firebase_6_28)\FirebaseAnalytics\GoogleDataTransport.xcframework\ios-armv7_arm64\GoogleDataTransport.framework;$(Firebase_6_28)\FirebaseAnalytics\GoogleAppMeasurement.framework;$(Firebase_6_28)\FirebaseAnalytics\FirebaseInstallations.xcframework\ios-armv7_arm64\FirebaseInstallations.framework;$(Firebase_6_28)\FirebaseAnalytics\FirebaseCoreDiagnostics.xcframework\ios-armv7_arm64\FirebaseCoreDiagnostics.framework;$(Firebase_6_28)\FirebaseAnalytics\FirebaseCore.xcframework\ios-armv7_arm64\FirebaseCore.framework;$(Firebase_6_28)\FirebaseAnalytics\FirebaseAnalytics.framework;$(Firebase_6_28)\FirebaseMessaging\FirebaseMessaging.xcframework\ios-armv7_arm64\FirebaseMessaging.framework;$(Firebase_6_28)\FirebaseMessaging\FirebaseInstanceID.xcframework\ios-armv7_arm64\FirebaseInstanceID.framework;$(Firebase_6_28)\FirebaseAnalytics\PromisesObjC.framework\ios-armv7_arm64\PromisesObjC.framework;$(Firebase_6_28)\FirebaseMLModelInterpreter\Protobuf.xcframework\ios-armv7_arm64\Protobuf.framework
      

       

  • (delphi) directive “-ObjC” to linker LD (project -> options -> Delphi Compiler -> Linking) to allow to include SDK methods
  • (delphi) copy the Delphi “iOSapi.FirebaseCommon.pas” file to your project folder ( C:\Program Files (x86)\Embarcadero\Studio\22.0\source\rtl\ios\iOSapi.FirebaseCommon.pas –> e:\progetti\myApp ) edit the file and write in it:
    • unit iOSapi.FirebaseCommon;
      
      {*******************************************************}
      {                                                       }
      {            CodeGear Delphi Runtime Library            }
      {                                                       }
      { Copyright(c) 2010-2021 Embarcadero Technologies, Inc. }
      {                  All rights reserved                  }
      {                                                       }
      {*******************************************************}
      
      interface
      
      uses
        Macapi.ObjectiveC,
        iOSapi.CocoaTypes, iOSapi.Foundation;
      
      const
        FIRInstanceIDErrorUnknown = 0;
        FIRInstanceIDErrorAuthentication = 1;
        FIRInstanceIDErrorNoAccess = 2;
        FIRInstanceIDErrorTimeout = 3;
        FIRInstanceIDErrorNetwork = 4;
        FIRInstanceIDErrorOperationInProgress = 5;
        FIRInstanceIDErrorInvalidRequest = 7;
        FIRInstanceIDAPNSTokenTypeUnknown = 0;
        FIRInstanceIDAPNSTokenTypeSandbox = 1;
        FIRInstanceIDAPNSTokenTypeProd = 2;
      
      type
        FIRInstanceIDError = NSUInteger;
      
        FIRAppClass = interface(NSObjectClass)
          ['{B8962096-555F-498E-B102-8EC66E871EF2}']
          {class} procedure configure; cdecl;
        end;
      
        FIRApp = interface(NSObject)
          ['{FFF4B247-25C6-47B8-BBC5-893D2170EFA5}']
        end;
        TFIRApp = class(TOCGenericImport<FIRAppClass, FIRApp>) end;
      
        FIRInstanceIDClass = interface(NSObjectClass)
          ['{4A9F1C85-AEDE-4284-A7DC-0EF9111504B1}']
          {class} function instanceID: pointer; cdecl;
        end;
      
        FIRInstanceID = interface(NSObject)
          ['{2967A1F9-98F5-40E6-8BDA-A25D3C699ED3}']
          function token: NSString; cdecl;
        end;
        TFIRInstanceID = class(TOCGenericImport<FIRInstanceIDClass, FIRInstanceID>) end;
      
      implementation
      
      uses
        System.Sqlite, System.ZLib,
        iOSapi.StoreKit;
      
      const
        libSystemConfiguration = '/System/Library/Frameworks/SystemConfiguration.framework/SystemConfiguration';
      
      procedure ClangRTLoader; cdecl; external '/usr/lib/clang/lib/darwin/libclang_rt.ios.a';
      procedure FirebaseAnalyticsLoader; cdecl; external  'FirebaseAnalytics';
      procedure FirebaseCoreLoader; cdecl; external  'FirebaseCore';
      procedure FirebaseCoreDiagnosticsLoader; cdecl; external  'FirebaseCoreDiagnostics';
      procedure FirebaseInstallationsLoader; cdecl; external  'FirebaseInstallations';
      procedure FoundationLoader; cdecl; external {$IFDEF IOS32}libFoundation{$ELSE}framework 'Foundation'{$ENDIF};
      procedure GoogleAppMeasurementLoader; cdecl; external  'GoogleAppMeasurement';
      procedure GoogleDataTransportLoader; cdecl; external  'GoogleDataTransport';
      procedure GoogleUtilitiesLoader; cdecl; external 'GoogleUtilities';
      procedure nanopbLoader; cdecl; external 'nanopb';
      procedure PromisesObjCLoader; cdecl; external 'PromisesObjC';
      //procedure PromisesObjCLoader; cdecl; external 'PromisesObjC';
      procedure SystemConfigurationLoader; cdecl; external {$IFDEF IOS32}libSystemConfiguration{$ELSE}framework 'SystemConfiguration'{$ENDIF};
      procedure StoreKitLoader; cdecl; external {$IFDEF IOS32}libStoreKit{$ELSE}framework 'StoreKit'{$ENDIF};
      
      end.
      

       

  • (delphi) copy the Delphi “iOSapi.FirebaseMessaging.pas” file to your project folder ( C:\Program Files (x86)\Embarcadero\Studio\22.0\source\rtl\ios\iOSapi.FirebaseMessaging.pas–> e:\progetti\myApp )
unit iOSapi.FirebaseMessaging;

{*******************************************************}
{                                                       }
{            CodeGear Delphi Runtime Library            }
{                                                       }
{ Copyright(c) 2010-2021 Embarcadero Technologies, Inc. }
{                  All rights reserved                  }
{                                                       }
{*******************************************************}

interface

uses
  Macapi.ObjectiveC,
  iOSapi.CocoaTypes, iOSapi.Foundation;

const
  FIRMessagingErrorUnknown = 0;
  FIRMessagingErrorAuthentication = 1;
  FIRMessagingErrorNoAccess = 2;
  FIRMessagingErrorTimeout = 3;
  FIRMessagingErrorNetwork = 4;
  FIRMessagingErrorOperationInProgress = 5;
  FIRMessagingErrorInvalidRequest = 7;
  FIRMessagingMessageStatusUnknown = 0;
  FIRMessagingMessageStatusNew = 1;
  FIRMessasingAPNSTokenTypeUnknown = 0;
  FIRMessasingAPNSTokenTypeSandbox = 1;
  FIRMessasingAPNSTokenTypeProd = 2;

type
  FIRInstanceIDAPNSTokenType = NSInteger;
  FIRMessagingAPNSTokenType = NSInteger;
  FIRMessagingError = NSUInteger;
  FIRMessagingMessageStatus = NSInteger;

  TFIRMessagingConnectCompletion = procedure(error: NSError) of object;

  FIRMessagingMessageInfoClass = interface(NSObjectClass)
    ['{FDAC534F-3D79-4FF6-824E-50DC7423662A}']
  end;

  FIRMessagingMessageInfo = interface(NSObject)
    ['{4D70F5C5-3635-405F-895C-F41C8D1FD76B}']
    function status: FIRMessagingMessageStatus; cdecl;
  end;
  TFIRMessagingMessageInfo = class(TOCGenericImport<FIRMessagingMessageInfoClass, FIRMessagingMessageInfo>) end;

  FIRMessagingRemoteMessageClass = interface(NSObjectClass)
    ['{EF45D074-C7A5-4DB2-BCD1-53B8650419F4}']
  end;

  FIRMessagingRemoteMessage = interface(NSObject)
    ['{6E2F8E14-FD8D-4B5D-8026-A607BE0B8F9C}']
    function appData: NSDictionary; cdecl;
  end;
  TFIRMessagingRemoteMessage = class(TOCGenericImport<FIRMessagingRemoteMessageClass, FIRMessagingRemoteMessage>) end;

  FIRMessaging = interface;

  FIRMessagingDelegate = interface(IObjectiveC)
    ['{264C1F0E-3EA9-42AC-9802-EF1BC9A7E321}']
    procedure applicationReceivedRemoteMessage(remoteMessage: FIRMessagingRemoteMessage); cdecl;
    [MethodName('messaging:didReceiveMessage:')]
    procedure didReceiveMessage(messaging: FIRMessaging; remoteMessage: FIRMessagingRemoteMessage); cdecl;
    [MethodName('messaging:didRefreshRegistrationToken:')]
    procedure didRefreshRegistrationToken(messaging: FIRMessaging; fcmToken: NSString); cdecl;
    [MethodName('messaging:didReceiveRegistrationToken:')]
    procedure didReceiveRegistrationToken(messaging: FIRMessaging; fcmToken: NSString); cdecl;
  end;

  FIRMessagingClass = interface(NSObjectClass)
    ['{62AF9A4C-681E-4BCD-9063-6209CAE08296}']
    {class} function messaging: pointer; cdecl;
  end;

  FIRMessaging = interface(NSObject)
    ['{A721C3D4-82EB-4A7B-A5E5-42EF9E8F618E}']
    function APNSToken: NSData; cdecl;
    procedure connectWithCompletion(handler: TFIRMessagingConnectCompletion); cdecl;
    function delegate: Pointer; cdecl;
    procedure disconnect; cdecl;
    procedure sendMessage(msg: NSDictionary; receiver: NSString; messageID: NSString; ttl: Int64); cdecl;
    procedure setAPNSToken(apnsToken: NSData; tokenType: FIRMessagingAPNSTokenType); cdecl;
    procedure setDelegate(delegate: Pointer); cdecl;
    function shouldEstablishDirectChannel: Boolean; cdecl;
    procedure setShouldEstablishDirectChannel(value: Boolean); cdecl;
    procedure subscribeToTopic(topic: NSString); cdecl;
    procedure unsubscribeFromTopic(topic: NSString); cdecl;
  end;
  TFIRMessaging = class(TOCGenericImport<FIRMessagingClass, FIRMessaging>) end;

  function kFIRInstanceIDTokenRefreshNotification: NSString; cdecl;

implementation

uses
  iOSapi.FirebaseCommon,
  Macapi.Helpers;

function kFIRInstanceIDTokenRefreshNotification: NSString;
begin
  Result := StrToNSStr('com.firebase.iid.notif.refresh-token');
end;

procedure FirebaseInstanceIDLoader; cdecl; external  'FirebaseInstanceID';
procedure FirebaseMessagingLoader; cdecl; external  'FirebaseMessaging';
procedure ProtobufLoader; cdecl; external 'Protobuf';

end.
  • Delete from the files the writing “{$ IFNDEF IOS32} framework {$ ENDIF}” (directive to the compiler) from the two files just mentioned
  • Include the Embarcadero libraries for managing pushes via FCM in the main project form
interface

uses xxxx
  ,System.PushNotification


  {$IFDEF ANDROID},FMX.PushNotification.Android {$ENDIF}
  {$IFDEF IOS}
     
       , FMX.PushNotification.iOS
       , FMX.PushNotification.FCM.iOS
     

  {$ENDIF}

..
..
..
uses System.NetEncoding, 
{$IFDEF IOS}
  ,iOSapi.Foundation
  ,iOSapi.CoreTelephony
{$ENDIF}

{$IFDEF ANDROID}
  ,Androidapi.Helpers,
  Androidapi.JNIBridge,
  Androidapi.Jni,
  Androidapi.JNI.JavaTypes,
  FMX.Platform.Android,
  Androidapi.JNI.Os,
  FMX.Helpers.Android


{$ENDIF}
;

  • We define in the form the methods of managing the notification reception and the connection change, we also use the onclick event “btnInitializePushClick” of a button “btnInitializePush” to perform the initialization of the notification management. The interface is managed in this demo by a “memoLog” Tmemo
      TfmxMain = class(TForm)
        ....
      private
        { Private declarations }
        o : TMyBridgeObject;
    
        // PUSH NOTIFICATION ID
        FDeviceId: string;
        FDeviceToken: string;
    
        function HandleAppEvent(AAppEvent: TApplicationEvent; AContext: TObject): Boolean;
    
        procedure btnInitializePushClick(Sender: TObject);
    
        {$IF (defined(ANDROID)) OR (defined(IOS))}
    
        procedure OnServiceConnectionChange(Sender: TObject; PushChanges: TPushService.TChanges);
        procedure OnReceiveNotificationEvent(Sender: TObject; const ServiceNotification: TPushServiceNotification);
    
        {$ENDIF}
    
      public
        { Public declarations }
        ....
    
      end;
  • We implement the method that manages the receipt of notifications
    {$IF defined(ANDROID) OR defined(IOS)}
    procedure TfmxMain.OnReceiveNotificationEvent(Sender: TObject;
      const ServiceNotification: TPushServiceNotification);
    var
      MessageText: string;
    begin
      MemoLog.Lines.Add('-----------------------------------------');
      MemoLog.Lines.Add('DataKey = ' + ServiceNotification.DataKey);
    
    
      MemoLog.Lines.Add('Json = ' + ServiceNotification.Json.ToString);
      MemoLog.Lines.Add('DataObject = ' + ServiceNotification.DataObject.ToString);
      MemoLog.Lines.Add('---------------------------------------');
    end;
    
    
    procedure TfmxMain.OnServiceConnectionChange(Sender: TObject;
      PushChanges: TPushService.TChanges);
    var
      PushService: TPushService;
    begin
      PushService := TPushServiceManager.Instance.GetServiceByName
        (TPushService.TServiceNames.FCM);
      if TPushService.TChange.DeviceToken in PushChanges then
      begin
        FDeviceToken := PushService.DeviceTokenValue
          [TPushService.TDeviceTokenNames.DeviceToken];
        MemoLog.Lines.Add('Firebase Token: ' + FDeviceToken);
    //    Log.d('Firebase device token: token=' + FDeviceToken);
      end;
      if (TPushService.TChange.Status in PushChanges) and
        (PushService.Status = TPushService.TStatus.StartupError) then
        MemoLog.Lines.Add('Error: ' + PushService.StartupError);
    end;
    {$ENDIF}
  • Push initiation via the button “click” event:
  • procedure TfmxMain.btnIniutializePushClick(Sender: TObject);
    {$IF (defined(ANDROID) OR defined(IOS))}
    
     var
       PushService: TPushService;
       ServiceConnection: TPushServiceConnection;
       Notifications: TArray<TPushServiceNotification>;
     begin
       PushService :=
           TPushServiceManager.Instance.GetServiceByName(TPushService.TServiceNames.FCM);
       ServiceConnection := TPushServiceConnection.Create(PushService);
       ServiceConnection.Active := True;
       ServiceConnection.OnChange := OnServiceConnectionChange;
       ServiceConnection.OnReceiveNotification := OnReceiveNotificationEvent;
    
       FDeviceId :=
           PushService.DeviceIDValue[TPushService.TDeviceIDNames.DeviceId];
       MemoLog.Lines.Add('DeviceID: ' + FDeviceId);
       MemoLog.Lines.Add('Ready to receive!');
    
       // Checks notification on startup, if application was launched fromcold start
       // by tapping on Notification in Notification Center
       Notifications := PushService.StartupNotifications;
       if Length(Notifications) > 0 then
       begin
           MemoLog.Lines.Add('-----------------------------------------');
           MemoLog.Lines.Add('DataKey = ' + Notifications[0].DataKey);
           MemoLog.Lines.Add('Json = ' + Notifications[0].Json.ToString);
           MemoLog.Lines.Add('DataObject = ' +
           Notifications[0].DataObject.ToString);
           MemoLog.Lines.Add('-----------------------------------------');
       end;
    
    
     end;
    {$ELSE}
     begin
    
     end;
    {$ENDIF}

    Step 3: Configure the project on firebase:

  • Add the app to the project by clicking on “Add App” and download the GoggleService-Info.plist file to your Delphi project directory.

  • In the settings section you have to upload the APN certificate that you previously generated on the apple portal in the “Keys” section and upload it to:

 

 

At this point, compile the application and run it on your device.

Inside memoLog you will find a JSON containing the deviceID, you have to copy it to test sending the first message from firebase.

 

Step 5) Send a test push message from the firebase console or your server

 

Copy the server key and perform a rest call as follows:

 

The JSON must also contain the “notification” object if you want the notification to arrive even when your app is not active:

 

{
    "to": "d-fmNb_zQ6u39hIOXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "priority" : "high",
    "message_id":"2205", 
    "time_to_live": 200,
    "notification":{
      "title":"Portugal vs. Denmark",
      "body":"great match!"
    },
    "data":{
        "body":"Ciao",
        "title":"Cisco"
    }
}

that’s all