Communications · Tips & Tricks · Uncategorized

iOS and macOS App Extensions with Delphi

In this article we are going to cover how to package iOS and macOS Application Extensions with your Delphi developed iOS and macOS application and interact with the Application Extension from Delphi using the Application Groups API.

Introduction

There are numerous features on iOS and the Mac that are only available using Application Extensions. App Extensions are essentially background processes or plugins that are bundled along with your main host application and installed simultaneously when your app is initially installed. Delphi doesn’t have any way to directly build App Extensions, but you can create them in XCode and you can consume them, bundle them and interact with them in your Delphi project.

Why would you need Application Extensions? Well there are many API capabilities on iOS and macOS that are only available through App Extensions. For example, you can use the ReplayKit API directly from your Delphi app to capture your own Delphi app UX experience and either record it or stream it over the web. However, as soon as you return to the home screen on iOS, your application is placed into the background and ReplayKit no longer provides the callback. If you want to capture the entire UX experience of the iOS device you need a background process. Apple created the concept of a Broadcast Upload App Extension which uses the ReplayKit APIs in the background to capture the entire device UX. This capability is only available from an Application Extension.

There are many other capabilities only available via App Extensions, such as Today widgets and Social sharing extensions.

Creating App Extensions

XCode provides templates to ease the process of creating an App Extension. You simply create a new Project in XCode and then add a new Target for the extension(s) you want to include. While the template provides no practical code, it is a working App Extension that you can manipulate and test the ideas presented in this article.

Apple expects that all App Extensions will use a Bundle Id that is an expansion of host application’s Bundle Id. So if your Delphi host application’s Bundle Id is com.company.myapp then your App Extension’s Bundle Id needs to be com.company.myapp.myextension.

Once you build the project in XCode, you only need to copy the App Extensions from the Mac over to your Windows computer where your Delphi project resides. Essentially it is as simple as locating your XCode bundle .app folder and copying the entire \PlugIns subfolder and all related files to a location in your existing Delphi project.

Once you have copied the extension(s), you only need to add these files to your Delphi Deployment Manager. They must target the .\PlugIns folder in your deployed project. When you Build and Run your Delphi application, your App Extensions will automatically be installed along with your main host application. It is really that simple.

The Grijjy DeployMan tool makes adding and removing files from the Deployment Manager settings for your Delphi project much easier.

Exchanging data with your App Extension

For security reasons, on iOS and macOS your application is essentially sand-boxed and it is prevented from interacting directly with other processes. This is also true of App Extensions and their host application, even if they share a Bundle Id and even if they are part of the same Bundle.

In order to initialize or interact between your Delphi created host application and the extension you need to use some form of IPC (inter-process communication). On iOS and macOS there is a mechanism known as XPC that provides various IPC related capabilities. Apple actually uses XPC to launch and configure App Extensions internally. However, for developers on iOS, Apple provides a set of APIs known as Application Groups which essentially uses the XPC APIs to provide a shared space for multiple App Extensions and the host application in the same bundle to interact and exchange information.

In order to use Application Groups you need to enable App Groups in the Apple Develop Portal and create a new App Id. First you create an App Group in the Apple Developer portal and provide a properly formatted name. This should be in the form of group.com.company.myapp. Then you create a new App Id for your main host application and for each of your App Extensions. They should each be configured to use App Groups and you should choose the Application Group name you created (ex: group.com.company.myapp). In Delphi you need to make sure your project’s Provisioning Profile (under Project Options) is configured correctly.

Delphi will automatically include the new Entitlements from the Provisioning Profile for the Application Group and Delphi will automatically add the Entitlement to your Bundle.

    <key>com.apple.security.application-groups</key>
    <array>
        <string>group.com.company.myapp</string>
    </array>

You should not add this Entitlement manually or your application will not function because of the duplicate key.

Using App Groups from your Delphi application

In order to share settings and data between your main host application and the App Extension, you need to use the NSUserDefaults APIs and initialize it with the initWithSuiteName() API. This is done in every host application and App Extension that wants to exchange data.

Delphi doesn’t expose this specific API, so for our purposes we will create a small wrapper to expose it:

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

type
  MissingNSUserDefaults = interface(NSUserDefaults)
    ['{57042F99-BD77-4E25-9AE1-B8F0C6776A18}']
    function initWithSuiteName(suitename: NSString): Pointer; cdecl;
  end;

  TMissingNSUserDefaults = class(TOCGenericImport<NSUserDefaultsClass, MissingNSUserDefaults>)  end;

Then to setup our shared settings from Delphi we first initialize the NSUserDefaults using our Application Group name along with the initWithSuiteName() API. You can only use theinitWithSuiteName() API to create a shared NSUserDefaults and all other related APIs will instead initialize a sandboxed NSUserDefaults. We then call setObject() or one of the various APIs available within NSUserDefaults to update a specific key/value pair. You can pass as many individual settings as you want, but in order to update all other App Extensions about the changes you must call the synchronize() API.

var
  Defaults: NSUserDefaults;
  SharedSettings: MissingNSUserDefaults;
begin
  { Init with the proper Application Group suite name }
  Defaults := TNSUserDefaults.Alloc;
  SharedSettings := TMissingNSUserDefaults.Wrap((Defaults as ILocalObject).GetObjectID);
  SharedSettings.initWithSuiteName(StrToNSStr('group.com.company.myapp'));

  { Save SomeKey/SomeValue to the shared settings }
  SharedSettings.setObject(
    (StrToNSStr('SomeValue') as ILocalObject).GetObjectID,
    StrToNSStr('SomeKey') );

  { Update the settings }
  SharedSettings.synchronize;
end;

Using App Groups from your Objective-C App Extension

From the App Extension in XCode, we can read these settings. The process in XCode is the same as Delphi in that need to use the NSUserDefaults APIs and initialize it with the initWithSuiteName() API.

NSString *const AppGroupName = @"group.com.company.myapp";

// Settings passed from the host application
NSString *someValue;

You need to supply your App Group name to the initWithSuiteName() API as shown below.

    // Create and share access to an NSUserDefaults object
    NSUserDefaults *sharedSettings = [[NSUserDefaults alloc] initWithSuiteName: AppGroupName];

    // Get shared object key/value
    someValue = [sharedSettings stringForKey:@"SomeKey"];

And finally you can extract the settings you created in your Delphi main host application from within your XCode authored App Extension.

Can App Extensions be created with Delphi?

It would be great if you could create App Extensions (.appex) with Delphi. Extensions are really just separate applications that are packaged with your main app and executed on demand. The big problem for Delphi produced apps and most other platforms outside of Objective-C with XCode is you are probably not going to get app store approval from Apple because you have to use private APIs to make it work, especially on iOS.

It is absolutely possible to create a bare-bones project in Delphi, make a few changes to the info.plist and the version info and build an application that acts like an app extension. This bare-bones app can be added to your Delphi Deployment manager and deployed along with your main app.

Those steps involve:

  1. Create a project group, consisting of a host app and an extension app.

  2. Create a FMX project for your “main” host app.

  3. Set your CFBundleIdentifier to something specific. (ex: com.myorg.MyApp )

  4. Create a FMX project for your app extension.

  5. Set your CFBundleIdentifier to an exactly match the bundle identifier of the host app and append something specific. (ex: com.myorg.MyApp.$(ModuleName) )

  6. Remove the Main form from the app extension project and all the units from the DPR.

  7. Modify the iOS setting, CFBundlePackageType from APPL to XPC! in the version information for the extension.

  8. Add the NSExtension settings to your info.plist:

    <key>NSExtension</key>
    <dict>
    <key>NSExtensionPointIdentifier</key>
    <string>com.apple.???</string>
    <key>NSExtensionPrincipalClass</key>
    <string>???</string>
       etc...
    </dict>
    
  9. Build and Run your extension app project so it is deployed to the Mac.

  10. Copy the compiled .app for your extension from the Mac to a folder on your Windows build machine.

  11. Rename the extension folder name from an .app extension to .appex extension.

  12. Add the extension .appex folder and all subfolders and files to the Deployment Manager in Delphi with a subfolder target of .\PlugIns\ on the remote machine.

  13. Build and run the host app and your application extension will also be installed.

This looks like it works, but in reality the OS will not allow you to use your app extension. Let’s look at the issues.

  1. First off, although your extension is sand-boxed, communication still needs to happen between your host app and your extension. Apple does this using a form a IPC (inter-process communication) using a set of APIs internally built around the XPC foundation apis.

  2. XCode doesn’t use the ApplicationMain entry point to launch extensions, it uses a special entry point _NSExtensionMain and links to it using a special flag -e. Extensions need to implement NSExtension and handle NSXPCConnection which is considered a private API. You can manually create a header translation for the private APIs in the Foundation unit for NSExtension and implement this yourself.

  3. Extensions are sand-boxed and can only communicate with their host app using IPC, which you must implement in your extension or you will not be able to communicate with your extension.

  4. Extensions must be small and fast, so you can forget including any FireMonkey units because of size limitations or anything that causes your extension to perform poorly.

  5. You can’t debug your extensions in Delphi and you probably cannot build Debug builds because the size of your extension must not exceed 16MB.

Conclusion

I hope you enjoyed this discussion on the topics of iOS and macOS Application Extensions and using Application Groups for inter-process communications. We don’t believe these topics have ever been covered before in the Delphi community, so we hope it helps you with your projects.

Here at Grijjy we have used the techniques in this article to build Delphi applications that use App Extensions created in XCode with our projects. If you are willing to experiment a little in Objective-C you can make it all work. Maybe someday Delphi will have the ability to create App Extensions as well, much like Xamarin, which would be really great and make Delphi even that much more useful for cross-platform development.

Let us know if you learn any other hidden or undocumented gems about Application Extensions.

7 thoughts on “iOS and macOS App Extensions with Delphi

  1. Thank you for such a great article, it’s exactly what I’ve been looking for.
    Have you tried the Extensions approach for Today widgets? I created an Xcode project with a today widget (works fine), but when I pull the Plugins folder into my Delphi project which uses the same bundle ID as the Xcode app, the widget appears on the Today dashboard but says “Unable to load”. Have you managed to get this to work successfully?

    Like

    1. I have not experimented with Today widgets. Are you using iOS or macOS for the widget? There are numerous reasons for an “Unable to load” from what I can see, mostly because the widget crashed. I would start by examining the XCode console to see what error is triggered from the widget. Also make sure you included all the deployed assets, etc. properly because anything missing might also cause a crash. Additionally examine the info.plist of the host app created in XCode to see if it added any other entries that you may need in your Delphi host app that are widget specific.

      Like

      1. Thanks Allen, I appreciate your reply. I’ve tried all of your suggestions but unfortunately with no success. The today widget I’m using the basic one auto-generated by Xcode with no changes, and I’m attaching everything from the PlugIns folder of the Xcode build folder into the deployment of my Delphi app using the DeployMan tool, which seems to include all files.
        The info.plist file and the Delphi plist contain the same entries, and the bundle Ids for the host app and widget match those in the Xcode version of the same.
        The widget doesn’t appear to be crashing in the console logs, but does display a message “Can’t request remote view controller that is blacklisted”. I’m going to keep looking into this, but thought it would be useful to leave this comment for anyone else who’s trying to implement widgets on iOS.

        Like

      2. Hi Allen, thanks for the suggestions. I looked back through the logs in more detail and noticed an earlier message saying “Couldn’t communicate with a helper application” which was the actual “crash” and helped me solve the problem.
        The extension has to be created in Objective-C to work, and mine was created in Swift. Creating a new extension in Obj-C fixed the problem and I now have a working Today Widget for my Delphi app.
        Thanks for your help Allen, and for a great article! I always enjoy reading Grijjy articles.

        Like

Leave a comment