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.
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:
- profile name: The name you want to have displayed to the user. The font name would be a good bet. Unfortunately, configuration profiles don’t support localized profile names.
- profile identifier: An internal identifier for the profile; important because it determines whether a new profile should replace an existing one or should be added. The documentation specifies a reverse-DNS style identifier, so you could use the bundle name of your app suffixed with “.Font”. Configurator uses the concatenation of the domain name of the machine it’s running on, unreversed, and a UUID, which doesn’t conform to the spec.
- profile uuid, font uuid: Globally unique IDs, which you can obtain from the
uuidgen
tool. - font name: The font name to be displayed to the user. Again, configuration profiles don’t support localized font names.
- font identifier: An internal identifier for the font. The documentation recommends using the profile identifier with an additional component, which could be derived from the font name. Configurator again constructs a string that’s not quite conformant.
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:
- unsigned.mobileconfig is the configuration profile you want to sign.
- signed.mobileconfig is the resulting signed configuration profile.
- certificate is the fingerprint of the signing certificate – look for the Subject Key Identifier entry in your certificate, take the associated key ID, and remove all white space from that ID, which should result in a string of 40 hex digits.
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.
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:
- Set up a web server within the app.
- Configure the server to respond to a request from Safari with the configuration profile, using a
Content-Type
header with valueapplication/x-apple-aspen-config
. - Start a background task to keep the app running in the background.
- Send a request to Safari to get the configuration profile from the app’s server. When receiving the profile, Safari automatically starts the installation. Once installation is done (or cancelled), control returns to Safari.
In order to return from Safari to the app automatically, additional steps are necessary:
- Define a custom URL scheme for the app so that Safari can return to the app (not its web server!) with a request.
- Configure the server to first send an HTML page that reloads itself periodically.
- Configure the server to send the configuration profile for the first reload, and a redirect to the app’s custom URL scheme for subsequent reloads.
- Respond to the custom URL scheme by shutting down the web server.
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.