Writing a component for iOS using Delphi XE2 and FireMonkey

By: Anders Ohlsson

Abstract: This article discusses how you can write an Accelerometer component for iOS using RAD Studio XE2

    Prerequisites!

This article assumes that you are familiar with the basics of writing components for Delphi. It also assumes that you have downloaded and played with my Accelerometer demo for iOS.

    Where are we now?

As you can see in the current demo, the code has a whole bunch of $IFDEFs in it. All of the accelerometer stuff is on the main form. What if you wanted to take it to a different demo? You could just copy and paste it I suppose, but a much better approach would be to encapsulate the accelerometer code in a component. The component will do nothing on Windows, and it will work the way it's supposed to on iOS.

For instance, the main form contains this:

procedure TForm1.FormCreate(Sender: TObject);
begin
  {$IFDEF FPC}
  UIAccelerometer.sharedAccelerometer.setUpdateInterval(1.0 / kAccelerometerFrequency);
  AccDelegatVar := AccDelegate.alloc;
  UIAccelerometer.sharedAccelerometer.setDelegate(AccDelegatVar);
  {$ENDIF}
end;

We shouldn't have to deal with setup code like this in project after project!

    Writing the component

Here is the code for the accelerometer in its entirety. Notice that all of the $IFDEF stuff moves into this single unit. It won't pollute your project anymore. Add this to a package, install into the IDE and you're off and running, or should I say accelerating! :)

unit Accelerometer;

{$IFDEF FPC}
{$mode objfpc}{$H+}
{$modeswitch objectivec1}
{$ENDIF}

interface

uses
  SysUtils, Classes, FMX_Types
{$IFDEF FPC}
  , iPhoneAll
{$ENDIF}
  ;

type
  TAccelerateEvent = procedure(x,y : Double) of object;

  TiOSAccelerometer = class(TFmxObject)
  private
    FOnAccelerate: TAccelerateEvent;
    { Private declarations }
  protected
    { Protected declarations }
  public
    { Public declarations }
    constructor Create(AOwner: TComponent); override;
  published
    { Published declarations }
    property OnAccelerate: TAccelerateEvent read FOnAccelerate write FOnAccelerate;
  end;

{$IFDEF FPC}
const
  kAccelerometerFrequency = 100.0; // Hz
{$ENDIF}

{$IFDEF FPC}
type
  UIAcceleration = objcclass external (NSObject)
  public
    function timestamp: NSTimeInterval; message 'timestamp';
    function x: UIAccelerationValue; message 'x';
    function y: UIAccelerationValue; message 'y';
    function z: UIAccelerationValue; message 'z';
  end;
{$ENDIF}

{$IFDEF FPC}
type
  AccDelegate = objcclass(NSObject)
    procedure accelerometer_didAccelerate(accelerometer: UIAccelerometer; acceleration: UIAcceleration); message 'accelerometer:didAccelerate:';
  end;
{$ENDIF}

{$IFDEF FPC}
var
  AccDelegatVar: AccDelegate;
{$ENDIF}

var
  MyAccelerometer: TiOSAccelerometer;

procedure Register;

implementation

{$IFDEF FPC}
procedure AccDelegate.accelerometer_didAccelerate(accelerometer: UIAccelerometer; acceleration: UIAcceleration);
begin
  if Assigned(MyAccelerometer) then
    if Assigned(MyAccelerometer.FOnAccelerate) then
      MyAccelerometer.FOnAccelerate(acceleration.x,acceleration.y);
end;
{$ENDIF}

constructor TiOSAccelerometer.Create(AOwner: TComponent);
begin
  inherited;
  {$IFDEF FPC}
  UIAccelerometer.sharedAccelerometer.setUpdateInterval(1.0 / kAccelerometerFrequency);
  AccDelegatVar := AccDelegate.alloc;
  UIAccelerometer.sharedAccelerometer.setDelegate(AccDelegatVar);
  {$ENDIF}
end;

procedure Register;
begin
  RegisterComponents('iOS', [TiOSAccelerometer]);
end;

end.

Let's break it down and go through each piece. First we declare an event type. When the accelerometer gives us acceleration data we want the X and Y components of it.

type
  TAccelerateEvent = procedure(x,y : Double) of object;

Now, the class itself. Nothing strange here, simply a field to hold the event handler, a published property so it's assignable in the IDE. And a constructor.

type
  TiOSAccelerometer = class(TFmxObject)
  private
    FOnAccelerate: TAccelerateEvent;
  public
    constructor Create(AOwner: TComponent); override;
  published
    property OnAccelerate: TAccelerateEvent read FOnAccelerate write FOnAccelerate;
  end;

This code is specifically here due to an issue with the current parser for the UIAcceleration callback type.

type
  UIAcceleration = objcclass external (NSObject)
  public
    function timestamp: NSTimeInterval; message 'timestamp';
    function x: UIAccelerationValue; message 'x';
    function y: UIAccelerationValue; message 'y';
    function z: UIAccelerationValue; message 'z';
  end;

Next up, our delegate. This is the object that contains a method that is getting called when there's new acceleration data.

type
  AccDelegate = objcclass(NSObject)
    procedure accelerometer_didAccelerate(accelerometer: UIAccelerometer; acceleration: UIAcceleration); message 'accelerometer:didAccelerate:';
  end;

We need an instance of it:

var
  AccDelegatVar: AccDelegate;

We need a global reference to refer to the current instance of TiOSAccelerometer. This could probably be solved by a singleton, but I'm choosing this way for now. More later.

var
  MyAccelerometer: TiOSAccelerometer;

The implementation of the accelerometer delegate looks as follows. If the global reference is assigned, and it has an assigned event handler, then call it with the accelerometer data.

procedure AccDelegate.accelerometer_didAccelerate(accelerometer: UIAccelerometer; acceleration: UIAcceleration);
begin
  if Assigned(MyAccelerometer) then
    if Assigned(MyAccelerometer.FOnAccelerate) then
      MyAccelerometer.FOnAccelerate(acceleration.x,acceleration.y);
end;

The constructor takes care of the setup. We set the update interval, create an instance of the delegate and assign it properly.

constructor TiOSAccelerometer.Create(AOwner: TComponent);
begin
  inherited;
  {$IFDEF FPC}
  UIAccelerometer.sharedAccelerometer.setUpdateInterval(1.0 / kAccelerometerFrequency);
  AccDelegatVar := AccDelegate.alloc;
  UIAccelerometer.sharedAccelerometer.setDelegate(AccDelegatVar);
  {$ENDIF}
end;

Register the component on the iOS tab in the IDE!

procedure Register;
begin
  RegisterComponents('iOS', [TiOSAccelerometer]);
end;

    Writing an app that uses the component

We can now take the original demo project and rip out all the $IFDEF'd code. Below I'll simply cover the pieces that change in our new demo project that's using the component instead.

We drop a TiOSAccelerometer component on our form. This will add the Accelerometer unit to our uses clause:

interface

uses
  ..., Accelerometer;

We double-click on the component, and we get an event handler added for OnAccelerate:

type
  TForm1 = class(TForm)
    ...
    procedure iOSAccelerometer1Accelerate(x, y: Double);
    ...
  end;

The Form.Create method becomes super simple. We do need to assign the active TiOSAccelerometer component to the MyAccelerometer global reference. It will now be able to get those callbacks with the acceleration data.

procedure TForm1.FormCreate(Sender: TObject);
begin
  ResetBallPosition;
  MyAccelerometer := iOSAccelerometer1;
end;

Finally, the OnAccelerate event in our case. Couldn't get any neater!

procedure TForm1.iOSAccelerometer1Accelerate(x, y: Double);
begin
  MoveBall(x,y);
end;

That's it. Enjoy!

    Download

Accelerometer component for iOS

My Accelerometer demo for iOS (Not using the component at the moment. Reader exercise!)

    Contact

Please feel free to email me with feedback to aohlsson at embarcadero dot com.

Server Response from: ETNASC01