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 :
-
1$(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:
-
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778unit iOSapi.FirebaseCommon;{*******************************************************}{ }{ CodeGear Delphi Runtime Library }{ }{ Copyright(c) 2010-2021 Embarcadero Technologies, Inc. }{ All rights reserved }{ }{*******************************************************}interfaceusesMacapi.ObjectiveC,iOSapi.CocoaTypes, iOSapi.Foundation;constFIRInstanceIDErrorUnknown = 0;FIRInstanceIDErrorAuthentication = 1;FIRInstanceIDErrorNoAccess = 2;FIRInstanceIDErrorTimeout = 3;FIRInstanceIDErrorNetwork = 4;FIRInstanceIDErrorOperationInProgress = 5;FIRInstanceIDErrorInvalidRequest = 7;FIRInstanceIDAPNSTokenTypeUnknown = 0;FIRInstanceIDAPNSTokenTypeSandbox = 1;FIRInstanceIDAPNSTokenTypeProd = 2;typeFIRInstanceIDError = 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;implementationusesSystem.Sqlite, System.ZLib,iOSapi.StoreKit;constlibSystemConfiguration = '/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 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
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
1234567891011121314151617181920212223242526TfmxMain = class(TForm)....private{ Private declarations }o : TMyBridgeObject;// PUSH NOTIFICATION IDFDeviceId: 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
1234567891011121314151617181920212223242526272829303132333435{$IF defined(ANDROID) OR defined(IOS)}procedure TfmxMain.OnReceiveNotificationEvent(Sender: TObject;const ServiceNotification: TPushServiceNotification);varMessageText: string;beginMemoLog.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);varPushService: TPushService;beginPushService := TPushServiceManager.Instance.GetServiceByName(TPushService.TServiceNames.FCM);if TPushService.TChange.DeviceToken in PushChanges thenbeginFDeviceToken := 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) thenMemoLog.Lines.Add('Error: ' + PushService.StartupError);end;{$ENDIF} - Push initiation via the button “click” event:
-
12345678910111213141516171819202122232425262728293031323334353637383940procedure TfmxMain.btnIniutializePushClick(Sender: TObject);{$IF (defined(ANDROID) OR defined(IOS))}varPushService: TPushService;ServiceConnection: TPushServiceConnection;Notifications: TArray<TPushServiceNotification>;beginPushService :=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 CenterNotifications := PushService.StartupNotifications;if Length(Notifications) > 0 thenbeginMemoLog.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}beginend;{$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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "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