Persisting Settings in Delphi iOS Applications

By: David Clegg

Abstract: Describes how to take advantage of the iOS Settings app and the NSUserDefaults class to persist user preferences for iOS applications written in Delphi

    Introduction

Persisting application specific settings is a task required by many applications. iOS has a Settings app which can be used as a global place to configure user preferences on a per application basis. This article will discuss how you can leverage this app, and the underlying iOS user defaults framework, when persisting preferences in Delphi iOS apps.

    The Settings app

The iOS Settings app can be used to view and change user configurable preferences for apps stored on your iOS device. It supports data in a variety of formats, and allows for grouping and hierarchical navigation of preferences. The Settings app can be leveraged by Delphi iOS apps to ensure your users can specify any configurable app preferences in a manner that is consistent with other iOS apps.

    Bundling settings with a Delphi app

In Cocoa apps created in Xcode, the way to specify settings associated with your app is to include a settings.bundle when compiling your app. For Delphi apps, the approach is no different. You can leverage Xcode to create the settings bundle, which you can refer to when specifying the resources to bundle in your compiled Delphi app.

    Creating the settings.bundle

In order to create the default preferences to distribute with your app, and also to tell the iOS settings app the format of the settings that your app provides, you will need to create a settings.bundle file to include in your compiled app. This file is actually a bundle of files that describe the individual pages of preferences supported. At the very least it will contain a Root.plist file, which is an XML file describing the format of the preferences supported by your app, and containing their default values. It can also contain additional .plist files, which would be the case if you built a hierarchical set of preferences designed to be accessed using child panes of the Settings app. In addition to this, it may contain one or more .lproj files, which contains localized string resources used when displaying the preferences to the user. And if your preferences specify that the iOS Slider control is to be used, the settings.bundle may also include image files to be associated with the Slider.

While you could create these files using a text editor, it is easier to leverage the inbuilt support that Xcode provides. To get started, from the Xcode menu select File->New->File. Select the Resource option in the left navigation pane, click on the Settings Bundle icon and click the Next button.

Hide image
Click to see full-sized image

You will be prompted to save the Settings.bundle file. If you have access to the file system where it resides, normally it would be simplest to save it in the root directory for your Delphi project. As we will be creating a sample Delphi application in a minute, save it in a temporary location, and you can copy it to your Delphi project directory once you have finished configuring the bundle.

This will present you with a seemingly empty window, containing an image of a building block. In order to get to the Root.plist file, click on Settings.bundle in the top navigator, and select Root.plist.

Hide image

You will be presented with an editor allowing you to edit the elements in the underlying Root.plist XML file. If you expand the Preference Items node, you will see an example of some of the types of settings that are supported by the iOS Settings app and underlying preferences framework. Discussing all the supported preference types is outside the scope of this article, so for more details see the Preferences section of the iOS Developer Library documentation.

As the intent of this article is to demonstrate how to bundle and read these settings, we will not be changing much here. Expand the first item, and change the Title attribute to “Delphi Settings Demo”. Then expand the second item, and change the Default Value attribute to “Hello from Delphi!”. You may also want to make a note of the value of the Identifier attribute, as you will need this to identify the preference item you wish to read or write at run-time. Once you’ve finished, select File->Save from the Xcode menu.

Hide image
Click to see full-sized image

    Deploying with a Delphi app

The next step is to associate the settings bundle files created above with your Delphi app. To demonstate this, we will create a sample Delphi HD FireMonkey application that will be used for the remainder of this article. From the Delphi XE4 main menu select File->New->Other…. Select the Delphi Projects node, click on the FireMonkey Mobile Application icon, and click the OK button. When prompted for the application type, select HD FireMonkey Mobile Application, and click the OK button. Save the project as DelphiSettings.dproj. Locate the Settings.bundle you created above, and move it to the directory where you saved the files for the Delphi application you’ve just created.

Select Project->Deployment from the Delphi main menu to open the Deployment Settings tab. The Deployment Settings tab gives us the flexibility to deploy different preference defaults for different devices and configurations, but for simplicity we’ll deploy our preference defaults only for the debug configuration on the iOS simulator. To do this, select the Debug configuration – iOS simulator option in the combo box in the toolbar.

Click the Add Files toolbar button, and you will be presented with an Open file dialog. You should see a Settings.bundle directory, as Windows should allow us to browse the .bundle file contents as if it was a standard directory. Open this directory, select the Root.plist file, and click the Open button. We will now have to adjust the Remote Path to ensure iOS can unbundle and find this file at runtime. Click twice on the Remote Path entry corresponding to the Root.plist file we’ve just added, and change the value to ./Settings.bundle/. Click the Add Files button again, open the en.lproj directory, select the Root.strings file, and click the Open button. This time set the Remote Path entry to ./Settings.bundle/en.lproj/.

    Querying settings at run-time

    Using the Settings app

Now that we have associated a settings bundle with our app, we will build and deploy it to the iOS simulator. This article assumes you have already created and configured a target platform for the simulator, and that an instance of PAServer is running on an OSX machine where Xcode resides.

Select Run->Run Without Debugging from the Delphi main menu, to build, deploy, and run your app on the simulator. Click the Home button, select the first screen of icons, and click on the Settings icon. When you scroll down you should now see an entry for your Settings app.

Hide image
Click to see full-sized image

Click on the Settings entry, and you will be presented with a view displaying all the settings in your app.

Hide image
Click to see full-sized image

    Reading and writing in code

The Cocoa framework provides the NSUserDefaults class, which can be used to access user preferences at run-time, as well as change these preferences outside of the Settings app. We will now incorporate this class into our app to demonstrate how to query these settings at run-time.

Switch to the Form Designer and add a TEdit (NameEdit), TSwitch (EnabledSwitch), TTrackBar (SliderBar), two TButton controls (ReadButton, WriteButton), and two TLabel controls. Position the controls as shown in the below screenshot.

Hide image
Click to see full-sized image

    Plan A

In the OnClick event handler for ReadButton, add the following code. You will also have to add iOSapi.Foundation to the uses clause of the implementation section.

procedure TMainForm.ReadButtonClick(Sender: TObject);
var
  lDefaults: NSUserDefaults;
begin
  lDefaults := TNSUserDefaults.Wrap(TNSUserDefaults.OCClass.standardUserDefaults);
  NameEdit.Text := UTF8ToString(
    lDefaults.stringForKey(NSStr('name_preference')).UTF8String);
  EnabledSwitch.IsChecked := lDefaults.boolForKey(NSStr('enabled_preference'));
  SliderBar.Value := lDefaults.doubleForKey(NSStr('slider_preference'));
end;

Run the app on the simulator, and click the Read button.

    Plan B

You were probably expecting that the default settings specified in the Root.plist file would have been reflected in the components on the form. But these defaults aren’t actually for the benefit of our app, and are only used by the iOS Settings app to tell it what default values to display to the user. The way Apple advises developers to deal with this is to use the registerDefaults method of the NSUserDefaults class. In order to do this for our app, add the following code to the forms OnCreate method

procedure TMainForm.FormCreate(Sender: TObject);
var
  lDict: NSMutableDictionary;
  lDefaults: NSUserDefaults;
begin
  lDict := TNSMutableDictionary.Create;
  try
    lDict.setValue(
      (NSStr('Hello from Delphi!') as ILocalObject).GetObjectID,
      NSStr('name_preference'));
    lDict.setValue(TNSNumber.OCClass.numberWithBool(True), NSStr('enabled_preference'));
    lDict.setValue(TNSNumber.OCClass.numberWithDouble(0.5), NSStr('slider_preference'));

    lDefaults := TNSUserDefaults.Wrap(TNSUserDefaults.OCClass.standardUserDefaults);
    lDefaults.registerDefaults(lDict);
  finally
    lDict.release;
  end;
end;

Once again, run the app on the simulator and click the Read button. This time you should see the controls on the form populated with the values specified above.

    Writing values

In addition to using the iOS settings app to write changes to your app preferences, you can leverage the NSUserDefaults class to create a custom UI for this in your app.

Add the following code in the OnClick event handler for WriteButton.

procedure TMainForm.WriteButtonClick(Sender: TObject);
var
  lDefaults: NSUserDefaults;
begin
  lDefaults := TNSUserDefaults.Wrap(TNSUserDefaults.OCClass.standardUserDefaults);
  lDefaults.setObject(
    (NSStr(NameEdit.Text) as ILocalObject).GetObjectID,
    NSStr('name_preference'));
  lDefaults.setBool(EnabledSwitch.IsChecked, NSStr('enabled_preference'));
  lDefaults.setFloat(SliderBar.Value, NSStr('slider_preference'));
end;

Run the app on the simulator, enter “Lets change this!” in NameEdit, set EnabledSwitch to Off, set SliderBar to approximately ¾, and click the Write button. Change the values in all the controls, and click the Read button. You should see the values you persisted reflected back in the controls.

Switch to the iOS Settings app, and you should also see these new values reflected there.

Hide image
Click to see full-sized image

Whilst in the iOS Settings app, change the value in the Name field to “Changed in Settings”, and click the Settings button in the Navigator to persist these changes. Switch back to the DelphiSettings app, and click the Read button. You should see this change reflected.

    Providing a familiar API

    Introducing TUserIniFile

While the above code will work, it would be better if Delphi developers could work with a familiar API. To facilitate this, I have created a TUserIniFile class, which wraps interactions with the NSUserDefaults class. A version of this class should have been installed as part of the Delphi XE4 samples, which by default will be located at C:\Users\Public\Documents\RAD Studio\11.0\Samples\Delphi\RTL\CrossPlatform Utils\Apple.IniFiles.pas.

Here is the public interface for the TUserIniFile class.

TUserInifile = class(TCustomIniFile)
public    
  function ReadBool(const Section, Ident: string; Default: Boolean): Boolean; overload; override;
  function ReadBool(const Ident: string; Default: Boolean): Boolean; reintroduce; overload;
  procedure WriteBool(const Section, Ident: string; Value: Boolean); overload; override;
  procedure WriteBool(const Ident: string; Value: Boolean); reintroduce; overload;

  function ReadString(const Section, Ident, Default: string): string; overload; override;
  function ReadString(const Ident, Default: string): string; reintroduce; overload;
  procedure WriteString(const Section, Ident, Value: String); overload; override;
  procedure WriteString(const Ident, Value: String); reintroduce; overload;

  function ReadInteger(const Section, Ident: string; Default: Integer): Integer; overload; override;
  function ReadInteger(const Ident: string; Default: Integer): Integer; reintroduce; overload;
  procedure WriteInteger(const Section, Ident: string; Value: Integer); overload; override;
  procedure WriteInteger(const Ident: string; Value: Integer); reintroduce; overload;

  function ReadDate(const Section, Ident: string; Default: TDateTime): TDateTime; overload; override;
  function ReadDate(const Ident: string; Default: TDateTime): TDateTime; reintroduce; overload;
  procedure WriteDate(const Section, Ident: string; Value: TDateTime); overload; override;
  procedure WriteDate(const Ident: string; Value: TDateTime); reintroduce; overload;

  function ReadDateTime(const Section, Ident: string; Default: TDateTime): TDateTime; overload; override;
  function ReadDateTime(const Ident: string; Default: TDateTime): TDateTime; reintroduce; overload;
  procedure WriteDateTime(const Section, Ident: string; Value: TDateTime); overload; override;
  procedure WriteDateTime(const Ident: string; Value: TDateTime); reintroduce; overload;

  function ReadFloat(const Section, Ident: string; Default: Double): Double; overload; override;
  function ReadFloat(const Ident: string; Default: Double): Double; reintroduce; overload;
  procedure WriteFloat(const Section, Ident: string; Value: Double); overload; override;
  procedure WriteFloat(const Ident: string; Value: Double); reintroduce; overload;

  function ReadTime(const Section, Ident: string; Default: TDateTime): TDateTime; overload; override;
  function ReadTime(const Ident: string; Default: TDateTime): TDateTime; reintroduce; overload;
  procedure WriteTime(const Section, Ident: string; Value: TDateTime); overload; override;
  procedure WriteTime(const Ident: string; Value: TDateTime); reintroduce; overload;
 
  procedure ReadSection(const Section: string; Strings: TStrings); override;
  procedure ReadSections(Strings: TStrings); override;
  procedure ReadSectionValues(const Section: string; Strings: TStrings); override;
  procedure EraseSection(const Section: string); override;

  procedure DeleteKey(const Section, Ident: String); overload; override;
  procedure DeleteKey(const Ident: String); reintroduce; overload;

  procedure UpdateFile; override;

  constructor Create;
end;

In addition to supporting the standard API supported by most TCustomIniFile descendants, it also exposes overloads which do not require a section name to be specified. If this overload is used, the setting is persisted under the root dictionary in the root.plist file. If a section name is specified, then all settings using that name will be stored in a dictionary with a key matching the supplied section name.

Lets take this new class for a spin. Add Apple.Inifiles.pas and Apple.Utils.pas (which has utility methods used by TUserIniFile) to the DelphiSettings project. By default these should be located in the C:\Users\Public\Documents\RAD Studio\11.0\Samples\Delphi\RTL\CrossPlatform Utils. Alternatively add this directory to your projects search path.

Add two new TButton controls to the form, name them ReadIniButton and WriteIniButton, and set their respective Text properties to “Read Ini” and “Write Ini”. Add the following code in the OnClick handler for ReadIniButton.

procedure TMainForm.ReadIniButtonClick(Sender: TObject);
var
  lIni: TUserIniFile;
begin
  lIni := TUserIniFile.Create;
  NameEdit.Text := lIni.ReadString('name_preference', 'Hello from Delphi!');
  EnabledSwitch.IsChecked := lIni.ReadBool('enabled_preference', True);
  SliderBar.Value := lIni.ReadFloat('slider_preference', 0.5);
end;

Add the following code to the OnClick handler for WriteIniButton.

procedure TMainForm.WriteIniButtonClick(Sender: TObject);
var
  lIni: TUserIniFile;
begin
  lIni := TUserIniFile.Create;
  lIni.WriteString('name_preference', NameEdit.Text);
  lIni.WriteBool('enabled_preference', EnabledSwitch.IsChecked);
  lIni.WriteFloat('slider_preference', SliderBar.Value);
end;

Run the app on the simulator. Change NameEdit to say “Changed via TUserIniFile”, set EnabledSwitch to On, move SliderBar to approximately ¼, and press the Write Ini button. Open the iOS Settings app and you should see these new settings reflected there.

Change the Name field to “Read from TUserIniFile”, and click the Settings button in the Navigator. Switch to the DelphiSettings app and click the Read Ini button. The change you made in the Settings app should now be shown.

Hide image
Click to see full-sized image

    Summary

With a little bit of work, it is possible to leverage the iOS Settings app to manage the application specific preferences of your Delphi for iOS apps. These preferences can be easily queried at run-time, either via the NSUserDefaults class, or by leveraging the TUserIniFile class available from the RAD Studio XE4 Demos repository.

The code for this article is available on CodeCentral. For simplicity it includes a copy of the Apple.IniFiles.pas and Apple.Utils.pas files current as at the time of writing this article. You may want to refresh these files from the RAD Studio Demos repository on SourceForge. By default the samples installed with Delphi XE4 should be configured to make this possible using the Subversion client of your choice.

Server Response from: ETNASC04