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 Extension Specific References
- Decision tree for how to get started
- Information on how to submodule out the Web Extension, if desired.
Safari Web Extensions really have 3 pieces, shown here from the 2020 WWDC talk Meet Safari Web Extensions
- A root/host app
- The app extension piece
- The actual web extension
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
- 2019
- 2020
- 2021
- 2022
- 2023
Helpful for Inter Process Communications:
- https://developer.apple.com/documentation/xcode/configuring-app-groups
- https://developer.apple.com/documentation/foundation/xpc/
- https://developer.apple.com/documentation/updates/xpc/
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
- No Extension
- No Existing App
- Care about the extension talking to the root app
- On both iOS and macOS.
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
- Have Extension
- No Existing App
- Care about the extension talking to the root app
- On both iOS and macOS
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
- No or Yes on existing Extension
- Existing App
- Don’t care about the extension talking to the root app
From inside existing Xcodeproj, depending on what your app supports.
Xcode > File > New Target > macOS > Safari Extension
Xcode > File > New Target > iOS > Safari Extension
Xcode > File > New Target > visionOS > Safari Extension
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
- No or Yes existing Extension
- Existing App
- Care about the extension talking to the root app
- On both iOS and macOS
From inside existing Xcodeproj, depending on what your app supports.
Xcode > File > New Target > macOS > Safari Extension
Xcode > File > New Target > iOS > Safari Extension
Xcode > File > New Target > visionOS > Safari Extension
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.
- AppGroup between Root process and Extension process in native app
- UserPrefs or shared files in the app group bundle.
- (fake unix domain sockets, type of shared file.)
- https://developer.apple.com/documentation/xcode/configuring-app-groups/
- XPCConnection between Root process and Extension process in native app
- An external server
- go out onto the web.
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"
Option 1: Symlink
- Pro: don’t have to keep pushing and pulling between the local copies.
- Con: tricky if will be working on project with others since it will untraceable which version of the extension files the App thinks its pointing to. From
man git-config
-
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.
- Pro: What version you’re working on is always known and traceable by the App repo and any of its clones.
- Con: Have to remember to push the changes around. Pushing the main repo does not push the submodule.
# 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
git -c protocol.file.allow=always submodule update --remote ExtensionFolder/Resources
git -c protocol.file.allow=always submodule update --remote --merge
-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.