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
- Configure the distribution of the app on the developer.apple.com portal and on https://appstoreconnect.apple.com/apps/ <appId>
- Add the management code on the Delphi application and import the firebase sdk for iOS
- Configure the project on firebase https://console.firebase.google.com/project/
- Download the “GoogleService-Info.plist” file from firebase, copy it to the project directory and insert it in the distribution list (deploy)
- 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