Getting started with Safari Web Extensions

This article is part of a series.

This initial starting post will focus on getting started information with regards to Safari Web Extensions specifically. This post has:

Safari Web Extensions really have 3 pieces, shown here from the 2020 WWDC talk Meet Safari Web Extensions

Triangular diagram of how the web extension, the native app extension and the native app itself all go together.

It used to be that there was a whole Safari App Extension framework that was and still is macOS only. Since 2020 Apple pushes the Web Extensions (javascript) for as much as possible. Web Extensions work cross platform, and cross browser if done correctly.

One doesn’t have to do any of the communication between the host app and the extension shown along the bottom of the image. The Web Extension can stand full on its own with the Xcodeproj as a delivery husk. If one already has a working Web Extension, this is the way. See Option 2

Read & Watch

It’s actually worth it to start with 2019 and watch through to 2023. The 2019 information shows the out of date-ish macOS only SFSafariApplication API, but it provides needed context for later changes.

Umbrella Link: https://developer.apple.com/documentation/safariservices/safari_web_extensions

Helpful for Inter Process Communications:

Ways to start

Picking the one of the auto generated projects can help avoid the hard to troubleshoot “The operation couldn’t be completed. (SFErrorDomain error 1.)” error when there is a mismatch between the native app and the extension information being passed to Safari. Which auto-generated option depends on what pieces exist already.

Option 1

Xcode > File > New Project > Multiplatform > Safari Extension App

Will have a UIKit app with 4 targets. This is a good way to start even if you want to use SwiftUI because it sets up the four separate targets needed to use AppGroups successfully on BOTH iOS and macOS. AppGroups are one way to allow the Extension process to talk to the Root App process.

Option 2

Use the tool:

xcrun safari-web-extension-converter $YOUR_EXTENSION_DIRECTORY_HERE

It will copy the extension into a new UIKit App, giving feedback on what does and does not work for a Safari Web Extension.

Option 3

From inside existing Xcodeproj, depending on what your app supports.

It is possible to just create the one new Target and add additional platform SDK support to it.

If the web extension exists, replace the contents of the Resources folder with the those files. It may be useful to still run the web extension through the xcrun tool first to see what errors pop us.

Option 4

From inside existing Xcodeproj, depending on what your app supports.

Depending on what the communication strategy will be for inter-process communication it may be possible to create one Target and add platform support it. If planning on using AppGroups macOS and iOS(& visionOS?) cannot share targets (leads to CFPrefsPlistSource/kCFPreferencesAnyUser problems), so at minimum do those separately.

If the web extension exists, replace the contents of the Resources folder with the those files. It may be useful to still run the web extension through the xcrun tool first to see what errors pop us.

Inter-process Communication (Optional)

If the web extension does not need to integrate into an existing App infrastructure, don’t. It’s a PITA.

But if you do want to, here are some options. Focusing on styles of inter-process communications that will work on both iOS and macOS.

Detaching Web Extension Development from App Development (Optional)

Delete (if have an extension) or move the ExtensionFolder/Resources folder from template.

cd PATH/TO/RootApp/ExtensionFolder
mv Resources ../../StandAloneExtension
cd ../../StandAloneExtension
git init . ; git add . ; git commit -m "Initialize repository"
core.symlinks:

If false, symbolic links are checked out as small plain files that contain the link text. ...Useful on filesystems like FAT that do not support symbolic links... The default is true...
cd PATH/TO/RootApp/
ln -s ../StandAloneExtension/ ExtensionFolder/Resources

Option 2: Submodule

Creating a submodule of a local folder makes it practical to go ahead and change files in Xcode, and then push the changes to their own branch of the local copy of the “real” repo for merging with the stand alone development work.

# Go to parent repo and add the submodule
cd PATH/TO/RootApp/
git -c protocol.file.allow=always submodule add ../StandAloneExtension/ ExtensionFolder/Resources

-c protocol.file.allow=always because the file: schema is under lock down on macOS. Safer that way.

in /PATH/TO/StandAloneExtension git repo create a new branch, but DO NOT CHECK IT OUT. in the submodules directory, fetch that branch and check it out.

cd /PATH/TO/StandAloneExtension
git branch forXcproj ##StandAloneExtension should stay on main. 
## Optionally:
# git remote add xcpjSubmodule /PATH/TO/RootApp/ExtensionFolder/Resources
cd /PATH/TO/RootApp/ExtensionFolder/Resources
git status #see it's its own little world.
git remote -v  #see our local folder is origin
git fetch
git checkout forXcproj
## if you already made changes in the submodule's main
## rebasing forXcproj will pull those over.
git rebase main 

Changes

Get changes

-c protocol.file.allow=always because the file: schema is under lock down on macOS. Safer that way.

Send Changes

cd /PATH/TO/RootApp/ExtensionFolder/Resources
# git add & commit, etc
git push 

Make sure that the origin local repo does not have the branch you’re trying to push to checked out or you will get an error like:

 ! [remote rejected] main -> main (branch is currently checked out)

This is why we have created a branch just for the Xcode project. This is also why adding the Xcodproj as a remote for ../StandAloneExtension might be a good idea, it enables pulling from there.

Summary

All of this before a lick of Swift or Javascript. Worth it.

This article is part of a series.