Lontar

Installing Fonts on iOS

Norbert Lindenberg
June 24, 2015

Update September 2020: This article describes the font installation mechanism that was introduced in iOS 7 and is still supported in iOS 13. However, iOS 13 introduced a new and far more user-friendly mechanism using CoreText API. See the WWDC 2019 presentation Font Management and Text Scaling for more information on that new mechanism.

iOS comes with a selection of fonts that cover the major writing systems of the world. Some apps, however, need to install additional fonts for system-wide use. Third party keyboards for iOS, for example, may enable input for writing systems that iOS doesn’t support, and such keyboards are only useful if they also provide fonts for their writing systems. This article describes how such apps can package and install fonts, based on my experience bundling the Ubud font with the Balinese Font and Keyboard app. Note that this is about system-wide use – if you need to bundle a font just for use within your own app, Chris Ching has a tutorial for that.

Testing the fonts to install

Before you enable an app to install a font, you should make sure that the font actually works on iOS in all the apps where your users will want to use it. There are two considerations: Whether an app will find the font, and whether it can correctly render text using the font.

For finding a font, there are generally two ways: An app requests the font by name (possibly after the user has selected the font from a list of available fonts), or the app or the rendering system look for a font, any font, that can render a given character or character sequence.

Requesting a font by name seems to generally work in iOS 8 for installed fonts – when apps ask for a font by name, iOS returns an instance, and when apps ask for a list of available fonts, iOS includes installed fonts. For apps using web views, a font requested in a CSS font-family property will be found.

Finding a font that can render a given character, however, has been problematic at least in Safari and other WebKit-based apps: before iOS 8.3, WebKit completely ignored installed fonts when looking for a font that would render characters, so characters that weren’t supported by built-in fonts or fonts named in font-family properties were always rendered as “tofu”, hollow rectangles. This was partially fixed in iOS 8.3: For some parts of the Unicode character set WebKit now looks for fallback fonts, for other parts it doesn’t (Balinese was one of the lucky scripts). Recent code changes in WebKit indicate that this will finally be completely fixed in iOS 9.

Balinese
Balinese text as rendered by Safari in iOS 8.2 (top) and in iOS 8.3 (bottom) when a Balinese font is installed but not requested by name.

For rendering text using a font, you’re usually fine with fonts for simple left-to-right scripts that don’t require glyph substitutions or positioning. For more complex needs, glyph substitutions and positioning are supported using two different font technologies in iOS: Apple Advanced Typography (AAT) and Microsoft’s OpenType. AAT is pretty well supported in native text views and in WebKit in iOS 8, but not in apps that use their own font rendering engines, such as Microsoft Word. The selection of fonts using AAT is quite limited, even though it’s the only font technology in iOS that’s powerful enough for complex scripts such as Balinese. OpenType is far more commonly used by font developers, but has so far only supported a small set of complex scripts (it’s only in Windows 10 that Microsoft adds a Universal Shaping Engine for previously unsupported scripts), and implementations of OpenType in iOS or in apps’s own rendering engines have their own limitations and problems.

To prepare a font for testing, package it in a configuration profile, as described in the next section, and then install the resulting profile from email or from a web server, as described in a later section.

Packaging fonts in configuration profiles

While support for third-party keyboards was highlighted as a new feature in iOS 8, support for third-party fonts received much less attention. This is probably because the support that exists was primarily targeted at enterprise customers: Fonts for use across apps are packaged in configuration profiles, which otherwise serve to configure virtual private networks, disable games and unsafe web sites, locate network printers, and do other things that matter in corporate environments. However, once the feature became available in iOS 7, it was fairly quickly used outside enterprises: Apps such as AnyFont enabled users to install their own fonts on iOS, and font vendor such as Hoefler & Co. let their users install their licensed fonts.

Let’s assume you have a font that you’d like to bundle with your app, and you have the license to do so. The font must be in TrueType (.ttf) or OpenType (.otf) format; configuration profiles don’t support font collections (.ttc, .otc). You can create a configuration profile containing the font with the Apple Configurator, or you can create a configuration profile template and use a script to insert the font.

To use the Apple Configurator, download and launch the app. Select the Supervise pane and the Settings subpane. Under the list of profiles, click “+” and choose “Create New Profile”. In the dialog that appears, give the new profile a name, such as the font name, and add your organization name if you like. Then scroll down the list on the left side and select “Fonts”, click “Configure”, and select your font. Click “Save”. Back in the profiles list, select your new profile, click the share icon (next to “–”), and save the profile to disk. Do not select the “Sign Configuration Profile” check box – we’ll discuss correct signing in the next section.

Using the Configurator has a few drawbacks: The process cannot be easily automated, so it doesn’t work well if you’re still developing the font. In addition, Configurator generates default values for some data over which you may want to have more control. Creating a template and inserting the font via a script avoids these issues.

A minimal template for a configuration profile looks like this:

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<plist version="1.0">

<dict>

<key>PayloadDisplayName</key>

<string>profile name</string>

<key>PayloadIdentifier</key>

<string>profile identifier</string>

<key>PayloadRemovalDisallowed</key>

<false/>

<key>PayloadType</key>

<string>Configuration</string>

<key>PayloadUUID</key>

<string>profile uuid</string>

<key>PayloadVersion</key>

<integer>1</integer>

<key>PayloadContent</key>

<array>

<dict>

<key>Name</key>

<string>font name</string>

<key>PayloadIdentifier</key>

<string>font identifier</string>

<key>PayloadType</key>

<string>com.apple.font</string>

<key>PayloadUUID</key>

<string>font uuid</string>

<key>PayloadVersion</key>

<integer>1</integer>

<key>Font</key>

<data>

</data>

</dict>

</array>

</dict>

</plist>

The parts you have to provide:

You also need to insert the font, in Base64 form, as the content of the data element. You can use this script, which takes as its arguments the template name, the font file name, and the name of the generated profile:

configurationProfileTemplateFile="${1}"

fontFile="${2}"

configurationProfileFile="${3}"

fontBase64File=`mktemp -t fontBase64`

base64 -b 52 -i "$fontFile" -o "$fontBase64File" || exit 1

sed -e '/<data>/ r '"$fontBase64File" "$configurationProfileTemplateFile" > "$configurationProfileFile" || exit 1

Configuration profiles can contain more information, such as descriptions of profile and font or the name of the company providing the profile. I’ve omitted them because they can’t be provided in localized form, and the descriptions that the profile installer in iOS provides seem adequate and are localized.

Signing configuration profiles

Configuration profiles can and should be signed. The signature confirms who created the profile, and that it hasn’t been modified. If a profile is not signed, the profile installer in iOS will warn the user multiple times, which is likely to deter some users from installing it.

Unfortunately, using Apple Configurator to sign the profile doesn’t help. Configurator creates and uses a self-signed certificate, which iOS doesn’t trust, so you get the same number of warnings, just saying “Not Verified” instead of “Not Signed”.

To correctly sign, you need a code signing certificate. As a registered iOS developer, I already have some code signing certificates on my key chain, issued by the Apple Worldwide Developer Relations Certification Authority. However, it turns out that iOS doesn’t trust them – it reports them as “not verified”. I ended up buying a COMODO code signing certificate from KSoftware, which cost US$95 and quite some time because their support for Macs is a bit flaky. You may be able to find a better deal and/or better support elsewhere.

To actually sign with your newly acquired certificate, there are at least two tools: openssl (available on many platforms) and security (Mac only). openssl requires that the private key for the code signing certificate is kept unencrypted in a file on disk, which I don’t like. security uses a certificate/key combination that’s stored safely on the OS X key chain, and so I ended up using this tool.

The magic incantation for security is:

security cms -S -H SHA256 -i "unsigned.mobileconfig" -o "signed.mobileconfig" -G -u 6 -Z certificate

where:

The security tool needs your permission to use the signing certificate from your key chain. If you run it from a terminal window, it opens up a dialog asking for permission, you approve, and all is well. If it’s run from a build script within Xcode, it may or may not be able to open the dialog, and may fail with a rather cryptic error message. It doesn’t indicate its failure with a non-0 exit code, and it still creates the signed.mobileconfig file, although it leaves it empty. To catch the failure and offer a path to recovery, I use the following script:

security cms -S -H SHA256 -i unsigned.mobileconfig -o signed.mobileconfig -G -u 6 -Z certificate

if [ ! -s signed.mobileconfig ]; then

rm -f signed.mobileconfig

echo "Could not create signed configuration profile signed.mobileconfig."

echo "To create it, please run the following command and click Always Allow when prompted:"

echo security cms -S -H SHA256 -i "'"unsigned.mobileconfig"'" -o "'"signed.mobileconfig"'" -G -u 6 -Z certificate

exit 1

fi

When you run the command from a terminal window and click “Always Allow” in the permission dialog, your permission also extends to future runs of security from within Xcode.

If all goes well, you should be greeted with a green “Verified ✓” under the name of the signer when installing the configuration profile.

Verified

Note that when your signing certificate expires, iOS will switch from “Verified ✓” to “Not Verified” in a second. There’s a time-stamp mechanism in code-signing that’s supposed to keep signatures valid that were created before the expiration of the certificate used, but that’s failing somewhere between the security command and the profile infrastructure in iOS. You’ll need to refresh your profiles with a new certificate to avoid verification issues. In iOS versions prior to 8.3 there was also a bug that may have led to the wrong signer being shown – this is now fixed.

I haven’t found a way yet to sign within Xcode server, which runs builds under its own user ID, which doesn’t benefit from the “Always Allow” permission that I give to the security command.

Installing configuration profiles from email and web sites

The documentation for configuration profiles lists five ways to install them, of which the most practical ones are through email or a web server.

To install a configuration profile from email, for example for testing, simply email it to an account that you can access from the test device. If you tap on the profile icon in the email received on the device, Mail switches to Settings, which offers to install the profile. After the installation is done (or cancelled), control returns to Mail.

To make a configuration profile available for installation from a web server, store it on the web server and make sure that the extension is .mobileconfig. The content type for configuration profiles is application/x-apple-aspen-config, but Safari doesn’t seem to care whether the server reports that correctly as long as the extension is there. If you point Safari to a URL with extension .mobileconfig and reasonable content, it switches to Settings, which offers to install the profile. After the installation is done (or cancelled), control returns to Safari.

Installing configuration profiles from within apps

If you want to enable installation of configuration profiles from within an app without depending on an external web server, or if you don’t want to make the configuration profile available on a web server at all, then things get a little difficult. There’s no API that enables installation of configuration profiles from within apps. A basic solution that developers have come up with involves multiple steps:

In order to return from Safari to the app automatically, additional steps are necessary:

My implementation is centered on a class MobileConfigServer, which is written in Swift 1.2 and available under the 3-clause BSD license. It assumes that you’re building RoutingHTTPServer with its prerequisite CocoaHTTPServer as a framework HTTPServerFramework with the following header file:

#import <UIKit/UIKit​.h>

 

// Project version number for HTTPServer.

FOUNDATION_EXPORT double HTTPServerVersionNumber;

 

// Project version string for HTTPServer.

FOUNDATION_EXPORT const unsigned char HTTPServerVersionString[];

 

// Public headers of the framework

#import "RoutingHTTPServer​.h"

#import "HTTPServer​.h"

To define a custom URL scheme for the app, add the following to its Info.plist file:

<key>CFBundleURLTypes</key>

<array>

<dict>

<key>CFBundleURLName</key>

<string>name</string>

<key>CFBundleURLSchemes</key>

<array>

<string>scheme</string>

</array>

</dict>

</array>

For name, you should use a unique reverse-DNS style identifier, such as the bundle name of your app with a dot-separated suffix. The scheme is normally a unique single word consisting of letters a-z, although RFC 3986 section 3.1 allows digits and some punctuation as well.

When it’s time in your app to install the font, load the scheme, the data for the configuration profile, and the name to be used in the user interface for the profile, and call this function:

func runServer​(scheme: String, mobileConfigData: NSData, mobileConfigName: String) -> Bool {

if fontServer == nil {

fontServer = MobileConfigServer​(returnURL: "\(scheme)://")

} else {

fontServer!.stop​()

}

if !fontServer!.start​(mobileConfigData, mobileConfigName: mobileConfigName) {

return false;

}

UIApplication​.sharedApplication​().openURL​(NSURL​(string: "http://localhost:\(fontServer!.listeningPort​())/start")!)

return true

}

Updating installed fonts

Installed fonts do not get updated automatically. When the user installs a new version of your app that includes a new version of the font, you likely have to remind the user to re-install the font. In addition, installing a new version of the font doesn’t make other apps that are already running and using the old version magically switch to the new version – terminating and restarting the app often works, but the only way to guarantee that the new version will be used is to have the user restart the device. Version numbers in the font that your app can check and version numbers in the profile that the user can check may turn out to be very useful.

Acknowledgments

Many thanks to Muthu Nedumaran, creator of the Sangam Keyboards, and Marc Durdin, co-creator of the Keyman apps, for reviewing a draft of this article.

Updates

2019-03-13: The MobileConfigServer source has been updated with workarounds for various issues introduced in newer versions of iOS, and to Swift 4.0. Its constructor takes several new arguments, as explained in the comments.