Creating a simple Safari App Extension in 10 minutes

Sai Koneru
8 min readAug 27, 2021

--

Safari app extensions can help to extend the web-browsing experience by bringing some of your app functionality to Safari browser

Photo by Carl Heyerdahl on Unsplash

Let’s start by creating a new Xcode project. Open Xcode, click on “Create a new Xcode project” and then select “Safari Extension App” template under “macOS” tab.

Safari extension app
Safari Extension App template

In the next screen enter project details i.e. Name, Bundle identifier etc. Make sure Type is set to “Safari App Extension” and Language is set to “Swift

New Xcode project
Creating new Xcode project

Click on next to create the project. Don’t worry about all the files and code that Xcode generates. For now build and run the app.

You should see a screen like this

Safari app extension
Build and run Safari app extension

Next open Safari browser and go to preferences (or press Cmd+,) to see preferences pane. You should see a new extension called “MySafariExtension Extension 1.0” added by the app under the “Extensions” tab

Safari extension preferences
Safari extension added to browser

Enable the extension by clicking the checkbox next to the extension name. You should see a new icon added to your toolbar as shown below

Safari extension toolbar icon
Safari extension toolbar icon

Congratulations!!! You have created your first Safari extension 🚀🚀

Getting into Action

OK! we have our extension up and running but it doesn’t do much. Clicking the toolbar icon doesn’t do anything. What is happening?

Let’s get to project and see the code that is generated by Xcode. Ignore the folder “MySafariExtension” this folder contains code to create a mac app (which is not the scope of this article). Our focus is only on the second folder called “MySafariExtension Extension” this is where all our extension code lives.

Go ahead and open the folder you should see the following files. We’ll go into detail about what each file does in the next section.

Safari extension folder structure
Safari Extension folder structure

Showing popover from toolbar icon

As we have seen, clicking on the toolbar icon doesn’t do much. Wouldn’t it be nice to show a popover when we click on the icon? Hmm.. yeah may be, but how can we do that?

There are couple of things we need to understand

SafariExtensionViewController

Firstly this is the view controller that is responsible for showing User Interface from the associated “SafariExentionViewController.xib” file. The .xib is used to build the user interface

Go ahead and open the xib file. You’ll see the interface builder with a Custom View and Label.

Safari extension interface builder
Safari Extension user interface

If you don’t see the document outline click on the icon next to “View as” in the bottom left corner

Go ahead and change the label text. Click on the label and open property inspector and change the title property to whatever text you want (alternatively double click on the label to change the text). Align the label in the centre of the view as shown below

Safari extension user interface
Safari extension user interface

Info.plist

OK, we have our interface ready. We want to display this when user click on the toolbar icon.

Open “Info.plist” file expand NSExtension key you should see a property called Action inside SFSafariToolbarItem property

Safari Extension Info.plit

Change the value of Action property from Command to Popover. This will show a popover when we click on the toolbar icon. After the change your Info.plist should look like this

Now go ahead and run the project. Open safari and click on the toolbar icon. You should the User interface that you built earlier

Safari Extension toolbar item popover

Awesome job!!! Next we’ll see how to add our own command to the context menu.

Adding items to Context Menu

What if we want to show an action item when the user right click (or control-click) on a webpage. The menu that appear when we right click is called a context menu

Safari context menu
Safari context menu

Let’s say we want to get the selected text from a webpage so that we can perform some action on the text (like translating or find meaning etc.). To do this we can add a context menu item

Go ahead and open Info.plist file and add the following property inside NSExtension dictionary. SFSafariContextMenu is responsible for adding new items in the context menu

Adding items to Safari context menu

Make sure the type for SFSafariContextMenu is set to Array. Add a new item in the array by clicking the plus icon and make the new item type (in this case Item 0) Dictionary. Then add to keys “Text” and “Command

Text is the name of the item that is shown in the context menu and command is what gets passed to the handler when the menu item is clicked.

Since SFSafariContextMenu is of type array, you can add multiple items.

Now if you run the project you don’t see the new context menu item. This is because by default our extension doesn’t have access to all webpages. To change this open “SFSafariWebsiteAccess” property within NSExtension and, delete “Allowed Domains” key and change the value for “Level” property from Some to All

Once you have done all the changes, your Info.plist file should look as follows

Safari Extension change website access

Now build and run the project. Safari will show the following dialog informing that our extension is requesting additional permissions.

Safari extension preferences
Safari extension preferences

Click on “Open Safari Extensions Preferences” and enable the plugin. At this point Safari asks if the plugin should be turned on, click on “Turn On”

Safari extension
Safari extension

Open any webpage and right click, you should see our new and shiny menu item added to the context menu

Safari context menu item

So far we haven’t written a single line of code to accomplish all this. Now it’s time to dive into some code to connect the menu item to the extension handler.

Connecting context menu item to code

Safari emit “contextmenu” event when the context menu is shown. So we need to listen to this event. Add the following code in script.js (inside Resources directory)

document.addEventListener(“contextmenu”, handleContextMenu, false);function handleContextMenu(event) {    let selection = window.getSelection().toString();    safari.extension.setContextMenuEventUserInfo(event, 
{ “selection”: selection });
}

Whenever context menu is presented, our “handleContextMenu” function is called. Inside the function we are getting the currently selected text and passing that information into userInfo dictionary.

Now we need to write a function in our swift code to receive the information from the extension when the context menu item is clicked. Thankfully Swift provides a function called contextMenuItemSelected and we can simply override this function in our SafariExtensionHandler class

Open SafariExtensionHandler.swift file and add the following code

override func contextMenuItemSelected(withCommand command: String, in page: SFSafariPage, userInfo: [String : Any]? = nil) {    switch command {        case “Find”:            if let userInfo = userInfo,
let text = userInfo[“selection”] {
NSLog(“Received text: \(text)”) } default: break }}

This function is called when our context menu item is clicked. If we have multiple menu items defined in the Info.plist, we can use a switch statement to execute code based on the command received.

Passing data back to the browser

Now that we got data from the browser, we can do whatever we want with that data. But how can we pass data back to the browser

We can use dispatchMessageToScript function to pass data back to the browser

Sending message from handler

Inside our SafariExtensionHandler we need to call dispatchMessageToScript function whenever we want to pass data to browser. Add the following code to SafariExtensionHandler.swift

func sendResponse(to page: SFSafariPage) {    page.dispatchMessageToScript(withName: “Find”,
userInfo: [“response”: “I am message from handler”])
}

Call the above function from contextMenuItemSelected function passing in the page as parameter. Your final code for contextMenuItemSelected should look like this

override func contextMenuItemSelected(withCommand command: String, in page: SFSafariPage, userInfo: [String : Any]? = nil) {    switch command {        case “Find”:            if let userInfo = userInfo,
let text = userInfo[“selection”] {
NSLog(“Received text: \(text)”) sendResponse(to: page) } default: break }}

Configure script to listen to messages

Finally we need to listen to “messages” event in the browser. When we send data to the script, this event is triggered. Add the following code to script.js file

safari.self.addEventListener(“message”, onMessageReceive, false);function onMessageReceive(event) {    window.alert(event.message.response);}

Whenever message event is triggered, onMessageReceive function is called. We get the data in the message attribute of the event object. So we can use event.message.response to get the response (response is the key we used in the dispatchMessageToScript function). For the purpose of demo we are showing it as an alert, but ideally you should do whatever action appropriate for your use case

If all goes well (if not please retrace the above steps) 😀 you should see the following dialog in your browser window when you click on Find Meaning menu item

Safari Extension dispatch message

Well done 🙌🏻🚀 you have learned how to create an extension, show UI when toolbar item clicked, add context menu, passing data between script and your handler code.

Use your new found knowledge to explore and create extension for your applications. Happy coding 👨‍💻👩‍💻

Get the code for this article https://github.com/yogasaikrishna/MySafariExtension

Thank you

Useful Resources

--

--

Sai Koneru

An avid reader 📚, programmer 👨🏻‍💻 currently living & working in Netherlands. I ❤️ reading books, writing / tinkering with code in my free time.