Motivation
Browser extensions have reached a certain momentum, hence Google announced a more strict review process and to remove extensions from the store by August 27th, 2020 which don’t comply to these new rules.
With an extensions based on the Mozilla WebExtension API, you can target all Chromium based browsers such as Chrome, Opera and Edge as well as FireFox. To also target Safari users, you require a dedicate implementation: a Safari App Extension. If you have not yet working in the Apple developer ecosystem, this is kind of a tough step, as there are multiple hurdles to take:
You need
- a Mac and XCode for development, as the App which you have to ship is compiled code, which will be distributed via the Apple App Store.
- a Developer Certificate for signing your code (comes for 99 EUR).
- to rewrite part of your existing extensions, if you not solely relying on content scripts.
To get some basic insights, I recommend working through these two tutorials before you start with your own Safari App Extension:
- Apple: Add, build, and enable a Safari App Extension
- Ulrik Lyngs: How to Build Safari App Extensions
Differences between Mozilla and Safari Architecture for Extensions
In a browser extension based on the Mozilla API there are different components, which you might need to port to Apple’s platform.
- Content Script: The content script(s) can be used almost unchanged. Safari also support these modules. But it wouldn’t be Apple if there is no difference to the standard. So there is no chrome or browser Object, but safari, which is slightly different.
- Background Script: The bad news is, you have to completely rewrite this functionality in Swift (or ObjC), as this functionality needs to be part of the native component. The good thing is, native code is fast, easy to test and Swift has very good support thru XCode. The background script will be replaced by the Extensions Handler.
- Popup: Yes, you can have Popups in Safari, if the App Icon in the toolbar of the browser is pressed, but it also requires a native implementation. (with a Storyboard). The correct term in Apple’s world is popover.
- Options Page: No, not supported. This functionality require you to implement this in a separate module as part of your native macOS application. (incl. Storyboards)
My Learnings
Common Code Base
Try to keep the codebase between Chrome and Safari as similar as possible for the content scripts. Refactor your existing content script, to have the browser specific functionality in separate classes/files. This is mainly related to the communication between the Content Script and the Extensions Handler (which is not present in Safari).
In order to figure out, whether your content script is running on safari or in a chrome environment you can use:
if (typeof safari !== 'undefined') {
console.log("+++ Running on Safari +++");
} else if (typeof chrome !== 'undefined') {
console.log("+++ Running on Chromium Platform +++")
} else {
console.log("+++ Running on some other Platform +++")
}
TypeScript Support
If you are working with TypeScript there is a type definition given for the safari Object. Unfortunately it does not contain the method signatures for safari.extension.* (but at least safari is now recognized).
Dealing with the Extension Handler
If your content script is sending a message to extension handler, the message will trigger the method messageReceived where you can process the data. For each message there will be a new instance of the extension handler, so you cannot share information between the extension handlers unless you make your variables static.
Project Targets
The project for a Safari App Extensions contains at least two targets:
- The native macOS application which is the container for distribution.
- The Safari App Extension which will integrate with the Browser.
Do not remove one or the other. Your App Extensions will neither run nor will it pass Apple’s review process.
Clean up generate code for native macOS App
If you are focusing on the Safari App Extension, the native macOS application will have almost no functionality. Still you might want to have a look at the application, as it is in the user’s application folder and of course it is part of the review process.
- Remove unused Menu Items (the auto generated view has a “Help” menu, without content)
- In the macOS app is a link to open the App Extensions tab in Safari. Make sure it works. (might be broken, if you refactor your application naming).
- Stick to the interface Guide Lines (the auto generated view does not have a title bar).
Remove headers from images, else the build breaks
You can use Vector graphics (PDF) or bitmaps (PNG) as toolbar icons. If you add images you have to remove the header for previews in Finder using xattr -cr on the command line. Else you will end up with Command CodeSign failed with a nonzero exit code.
CodeSign xxx.app (in target ‘xxx’ from project ‘xxx’)cd xxxexport CODESIGN_ALLOCATE=/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/codesign_allocateSigning Identity: “Apple Development: xy@gmx.at (xxxxxxxx)”/usr/bin/codesign — force — sign F50F8AC5AE578F274F67697238501C192F618474 -o runtime — entitlements xxx — timestamp=none xxxx.appxxx.app: resource fork, Finder information, or similar detritus not allowedCommand CodeSign failed with a nonzero exit code
Side note: Toolbar icons have only one color and a transparent background.
Creating a Popover
To create a popover four steps are required.
- In Info.plist, in the subtree SFSafariToolbarItem change the “Action” from “Command” to “Popover”.
- In the build target for the extension, in “General” select the XIB File for the Main Interface.
- You require an according implementation of the Controller; but there is a standard implementation as part of the generated XCode Project.
import SafariServicesclass SafariExtensionViewController: SFSafariExtensionViewController {
static let shared: SafariExtensionViewController = {
let shared = SafariExtensionViewController()
shared.preferredContentSize = NSSize(width:320, height:240)
return shared
}()
}
- In the Extensions Handler overwrite the popoverViewController method:
override func popoverViewController() -> SFSafariExtensionViewController {
return SafariExtensionViewController.shared
}
AppStore Review
A short view with english audio explanations will help the reviews to better understand the functionality of your extensions. You can simply upload it to AppStore Connect. Screenshots of the Extensions must be made with Safari.