Web Extensions in iOS

Joel Oliveira
Joel Oliveira
Nov 26 2021
Posted in Engineering & Technology

An intro into Safari Extensions in iOS

Web Extensions in iOS

With iOS 15 a new powerful feature is now available for Safari. If you are not familiar with Web Extensions, they are plugins that browsers can install that allow you to customize your browsing experience. For example, a browser extension can help users hide ads, prevent offensive content from being displayed, populate forms with your data, etc.

They're built with the standard web technologies: HTML, CSS, and JavaScript. And the WebExtension API has been available for all the major desktop browsers for a while, which means you can write a browser extension once and deploy it across all those browsers. In this post, we will focus on how you can bring an existing extension or build a new one with support for Safari iOS.

Before we start, it's important to mention that for Safari, Web Extensions are parts of apps, and like other types of apps, apps with Safari Web Extensions are available in the App Store. And pretty much the same way, Xcode has everything you need to develop Safari Web Extensions.

Creating a new Extension

The easiest way to start building a new extension is by creating a new Xcode project and use the Safari Extension App template to start building a web extension as well as its containing app:

And when you use this template, you get an extension that comes complete with all the resources a typical web extension might have. You can use this to bootstrap your new extension by customizing what's already there or adding or removing bits according to your needs:

Adapting a macOS Extension for iOS

Thanks to a tool called Safari Web Extension Converter, you can upgrade that project so it supports iOS too. This is done directly from the command line using the following command:

xcrun safari-web-extension-converter [options] --rebuild-project <path-to-your-project>

The converter will add an iOS-compatible version of the extension and a containing app to your project.

Porting Another Browser Extension to macOS/iOS

If you've already built a Web Extension for another browser, you can also take advantage of the Safari Web Extension Converter to automatically create an Xcode project from that existing extension. This is done using the following command:

xcrun safari-web-extension-converter [options] <path-to-extension>

This will create a new Xcode Project that, by default, references your existing extension's resources at the original path rather than copying them.

Debugging

As you make changes to your extension, you simply run your app again to update your extension in a physical device or simulator. A great way to identify potential problems, while in development, is by taking a look at the phone or simulator's Settings > Safari > Extensions > Your Extension menu:

If any issues arise, this is where you can first encounter some information about your extension's potential problems.

Additionally, as you add functionality or tweak the design of your extension's underlying content, you will want to inspect those elements, pretty much the same way as you would when developing a website. Thanks to the Safari's Web Inspector, this is also possible. All you need is Safari on your desktop and the Develop menu enabled. To enable it, go to Safari > Preferences and toggle the following checkbox ON in the Advanced tab:

After that, anytime you run your extension, you can debug it using the Develop menu in Safari:

Just like you would for a standard web page, you can inspect your extension code as shown below:

Caveats

Now let's dive into some of the things you might need to watch out for, as you build an extension for Safari in iOS.

Non-persistent background pages

Let's start with non-persistent background pages. A background page is a web page that the browser loads to run your extension's background script. This page allows your extension to handle events sent by the browser or other parts of your extension. But keeping this page loaded has a performance cost. This is especially important because background pages must be non-persistent on iOS, where system memory and battery life are at a premium.

To make sure you comply with this requirement, simply add a persistent key in your extension manifest as follows:

...
"background": {
    "scripts": [ "background.js" ],
    "persistent": false
},
...

Responsive Design

As you would expect, bringing an extension to iOS means its web content may be rendered in new devices with smaller screens. Just like a web page, you will need to adapt your content to display correctly in different formats. One common issue will arise if your extension contains a page with full screen content, where your content might cross the device's safe area. To address this issue, you will want to use the viewport meta tag, as follows:

<meta name='viewport' content='initial-scale=1, viewport-fit=cover'>

This is described in more detail here.

Pointer Events

If your extension currently depends on handling mouse events using the Mouse Events API, be aware that these events won't be fired in iOS. You should instead adopt the Pointer Events API. This means that you need to replace this:

element.onmousedown = function(e){}

With this:

element.onpointerdown = function(e){}

Just like the Mouse Events API, the Pointer Events API also reports touches and Apple Pencil input.

Windows API

If your extension uses the Windows API, you should know that each scene of Safari actually has two windows: one for regular browsing and one for Private browsing. This is also true for iPhone. This means that invoking the following in your extension:

await browser.windows.getAll();

Will actually return 2 objects:

[{
		"id": 1,
		"state": "normal",
		"incognito": false,
		"focused": true
	},
	{
		"id": 2,
		"state": "normal",
		"incognito": true,
		"focused": false
	}
]

This works exactly the same on iPad where one Safari scene is represented by two windows. But if you open a second scene of Safari in split view, the API would now report four windows. It is important to understand this, because if your extension relies on the windows.onCreated event, you will see that event being fired twice.

Feature Detection

When adding iOS support in your extension, you will find out that some APIs are unavailable. For example, some methods of the windows APIs mentioned above, are not available. But there are a few others, like context menus and WebRequest. If non-essential parts of your extension use such APIs, be sure to use feature detection to handle those cases where they will not be available.

This means you need to add conditionals that check for the existence of those APIs, as follows:

if (browser.webRequest) {
    browser.webRequest.onCompleted.addListener((e) => {...});
}

User Privacy

Just like in many other things in their platforms, User privacy is a huge part of everything Apple does. Historically, other browsers would give an extension full access to every website right away when you turn it on. But with Safari, users have total control over Web Extension and extensions are only given access to websites when the user consents.

And this consent is required for any privacy sensitive APIs to work. Specifically, any API that reveals the URL or title of a tab will only include this info if your extension has permission for that URL. For example, the Cookies API will only let your extension read and write cookies for websites your extension has permission for. And injecting JavaScript and stylesheets will only be allowed on websites where your extension has permission.

Getting Started

This is just a few key things you need to know to bring your web extensions to iOS. For a complete overview of everything that is needed for web extensions to work in both desktop and mobile Safari, we recommend you take a look at Apple's guides located here.

As always, we hope you liked this article and if you have anything to add, we are available via our Support Channel.

Keep up-to-date with the latest news