DEV Community

DEV Community

Bing Qiao

Posted on Jan 19, 2022

Creating a browser extension for Safari and Chrome

Projects diagram

Safari extension requires a Swift project that contains iOS/macOS parent apps plus their extension apps that share a bunch of JavaScript and other resources.

The extension from my first attempt here was a crude implementation in plain, vanilla JavaScript. There was no bundling, minifying, framework or typing. There wasn't even a separate JavaScript project. All JavaScript&resources belonged to the Swift project and were managed by Xcode.

After some more research and learning, I recreated the same extension using React/TypeScript, not just for Safari but Chrome too. The new project uses esbuild to create bundled and minified code.

The extension project for Safari and Chrome

A much stripped down version of the extension resources project is hosted here browser-ext-react-esbuild while the container app for iOS/macOS is hosted here browser-ext

The first issue I had to address was how to create a Web extension using React/TypeScript/esbuild. Luckily there is already a template project that does exactly just that. esbuild-react-chrome-extension

The next issue is how to code in TypeScript against Web extension API for both Safari and Chrome. As it turns out Safari and Mozilla Firefox are very similar in their API but there are enough differences between them and Chrome to require different treatment especially when it comes to the use of "callbacks" and "promises" Building a cross-browser extension

Initially I created wrapper functions to convert Chrome functions that require callback to return promise instead. The better approach, as I found out later, is probably to use webextension-polyfill from Mozilla and its types .

A caveat here is, I had to set module in "tsconfig.json" to "commonjs" as shown below:

Then do import assignment in JavaScript files that call extension API:

Using import like below didn't work for me:

The code generated by esbuild for the import above calls __toESM for require_browser_polyfill() which renders the polypill proxy ineffective.

The container Swift project for Safari

Another issue is how to manage the React/extension project with the container Swift project.

The boilerplate extension resources (JavaScript/css, manifest and html files) created with a new Safari extension project are managed by Xcode. But I need them to be simply copied over from the React project, instead of having Xcode creating reference for every JavaScript/html/css/image file that needs to be part of the bundle it creates.

How extension resources get added to Swift app bundle by default

The problem is, we might have different files from the React project depending on whether it's a prod or dev build, especially if the bundler (such as Parcel) used generates randomised file names.

Create a new empty folder

Finally, add the folder to Copy Bundle Resources build phase. This needs to be done for both iOS and macOS extension targets.

Now, all it takes to import new extension resources from the React project is to copy everything over to Resources/build folder in the Swift project.

The two sample projects are setup to work together as long as they are checked out side-by-side in the same directory.

Now you can develop and test the extension against Chrome solely in the extension resources project. To test against Safari, just run an npm command to build extension resources and copy contents of dist to the container Swift project, then build/run it in Xcode.

The mechanism

auto-refresh

Auto-refresh is implemented using setTimeout() , browser.tabs.reload() and browser.storage.local .

  • Every managed (marked to auto-refresh) browser tab has an entry in a map persisted in extension storage local: tabId: boolean ;
  • Upon loading, content.js looks up its tabId in this map;
  • If there is an entry and the result is true , content.js will set up a timer of fixed interval (obviously the interval can be exposed to users too) to send a runtime message to background.js , asking for reload;
  • background.js receives the request and reloads the sender tab via browser.tabs.reload() .

The approach above is different from my first attempt on auto-refresh extension:

  • I was using a variable in background.js to hold tabs states which proves problematic. In Safari iOS, property persistent for background.js in manifest.json needs to be false , which means it can and will get reloaded. That explains why the extension was losing tab states whenever iPhone screen went dark. Using browser.storage.local seems to be the only viable alternative to tackling this issue, even though it adds quite a bit of complexity to the code base.
  • Using browser.storage.local , I now have to figure out a way to clean up tabs states once the browser is restarted. This is a bit tricky for Safari which does not implement extension session API. The approach I used is to do a clean up in browser.runtime.onStartup event. This seems to work well but I'm not certain how water-tight this is.
  • I was using window.location = window.location.href to do the reload. The better way is to call extension API browser.tabs.reload() which allows bypassCache argument to bypass browser cache.

Being able to test Safari extension easily in Chrome during development has saved me a lot of time. I'd be interested to hear if you have different approaches to some issues raised here.

Top comments (0)

pic

Templates let you quickly answer FAQs or store snippets for re-use.

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink .

Hide child comments as well

For further actions, you may consider blocking this person and/or reporting abuse

paulbratslavsky profile image

Epic Next JS 14 Tutorial Part 7: Next.js and Strapi CRUD Permissions

Paul Bratslavsky - Apr 17

mattlewandowski93 profile image

Creating a Planning Poker Demo app 🃏🕹️

Matt Lewandowski - Apr 16

gergelyszerovay profile image

Angular Addicts #24: Angular 17.3, Signals and unit testing best practices, Storybook 8 & more

Gergely Szerovay - Apr 17

ashray_sam profile image

Portfolio Website / Bible App - Devlog #9

K. Sam Ashray - Mar 14

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

  • Oct 27, 2021

How to Build a Safari App Extension in iOS 15

iOS 15 finally introduced Safari extensions, one year after Apple brought extensions to Safari on the desktop. Of course, they did it in a very gated, Apple-like way, which makes the process to develop a Safari extension much more difficult than it needs to be. Notably however, you still can't make Chrome extensions on iOS (or Android).

A few weeks ago I set out to build a Safari extension for my app, hbd (you can check out the extension now in the latest update). I didn't find any thorough guides for this like I usually do for other development processes, so I decided to make one.

Let's talk about the practical details of building a Safari Extension on iOS.

Before Beginning

There are some important things to know before developing a Safari extension on iOS.

Extensions must have a containing native application. You can't just upload a Safari extension like you can on the Chrome web store, you have to build an iOS app too (even if it's just a shell for your extension), and go through the whole App Store review process to get it published.

Lots of browser APIs are restricted/unavailable . Most of this is due to the fact that iOS has much stricter memory limits than a computer, so they've added requirements like non-persistent background pages. Read the list to make sure you're not planning to use any restricted APIs.

Tabula Rasa

If you want to start from scratch, Xcode has a project template for Safari App Extensions. Create a new project, then select Safari Extension App. I recommend doing this just to see what the structure of the extension looks like, but I couldn't actually use this in my case, since I had an existing Xcode project that I wanted to add a new extension to.

safari extension swift tutorial

If you already have an Xcode project, go to File->New->Target , and select Safari Extension (NOT Safari Extension App). Xcode will add a few boilerplate files in a new group:

SafariWebExtensionHandler.swift is the glue between your native app and the JS code. It implements the NSExtensionRequestHandling protocol, which allows it to receive and respond to messages from the JS.

the Resources/ folder contains all of your extension source code. This should be structured like an unpacked browser extension, with manifest.json at the top level.

safari extension swift tutorial

The boilerplate JS files are okay if you're making a basic extension, but won't help very much if you're using a build system and framework like React, which I assume most complex extensions do. If so, you can delete all the files inside the Resources/ folder (we'll replace them later).

If you have an existing Chrome extension that you want to convert to Safari, you can run this command-line tool to convert it (I didn't use this since I was starting my extension from scratch).

Using React

I built my extension using React + Typescript. Coming from a background in Swift, I would highly recommend using Typescript over regular Javascript. If you use SwiftUI, I would also recommend using React. React has a lot of similarities to SwiftUI making it easy to pick up, although it's just a poor man's version of SwiftUI 😎. Here is the basic structure of my code:

safari extension swift tutorial

If you use this boilerplate, run npm install after you've cloned it to download all the dependencies. Then, running npm run dev will build the extension and output it to the dist/ subdirectory. This dist/ directory is what we want to tell Xcode about.

Xcode - Groups & References

Xcode's file navigator (in the left pane) shows you a sort of pseudo-file structure of your project. I say pseudo- because it doesn't always represent what's actually stored on your hard drive. There are two ways to add new folders to an Xcode project - using groups or folder references.

If you add a group, new files added to that folder in your Finder don't automatically get added to your Xcode project. In Xcode 13, groups are represented with a gray folder icon.

If you add a folder reference, any new files added to that folder will automatically be added to your project. In Xcode 13, folder references are represented with a blue folder icon.

After researching this, I decided that using folder references is better than using groups. For example, if we add a new image to our extension's icons/ folder, we'd like that image to automatically be added to the Xcode project.

Here's how to accomplish this: No matter where you place your extension directory on your computer, you can open its dist/ subfolder and select all of the files, then drag them into the Resources/ folder in Xcode (NOT finder). Once you do this, a popup will appear:

safari extension swift tutorial

Make sure you are not selecting Copy items if needed , and that you are selecting Create folder references. If you select copy items, then all of the files from your dist/ folder will be unlinked from your source code. So if you make some changes and rebuild your extension, those changes won't be included in Xcode.

safari extension swift tutorial

If you open up the Resources/ folder in finder now, you will see that there are actually no files there. All of the files are actually stored in the dist/ folder in your extension directory, and are only referenced in Xcode and shown in the pseudo-file navigator. You can confirm this by opening the right pane (⌘⌥0) and looking at the file path for your JS files.

Unfortunately, we can't make the top-level Resources/ folder a folder reference in Xcode; it must be a group. This is a special folder name in Xcode and for some reason they won't let you change it to a folder reference. So if you add a new file to the top-level of your extension's dist/ folder (say you decide to add a new content script), you'll have to drag it into Xcode too. This is one change I would like Apple to make for future releases.

The Development Cycle

After you've set up the files, you can now start developing and testing your extension. The first step is to create a new scheme that runs your extension target:

safari extension swift tutorial

Every time you run this scheme, it will ask you which app to launch; select Safari. There may be a way to set this automatically, but I haven't figured it out yet. Using this scheme, you can profile your Extension's memory usage and set breakpoints in your SafariWebExtensionHandler class.

SafariWebExtensionHandler.swift is responsible for handling all messages sent from the JS scripts. The app can only listen and respond to messages; it can't initiate messages to the JS on its own. To send messages to your app, you need to add the nativeMessaging permission in the manifest.json, and use this code :

You should send native messages sparingly, as I'll discuss later on. The main reason to use them would be to request necessary data from UserDefaults or a shared keychain.

Hot Reload?

Web developers are used to hot reload, but that won't quite work here. I used npm run watch to rebuild my extension every time I changed a file, but that will only change what is in the dist/ folder (which is referenced in Xcode). It won't push your new changes on the device you're testing. To do that, you need to re-run the project in Xcode. This is a painfully slow experience. As a sort of compromise, you can download the React Preview plugin for VS Code. It works sort of like the canvas in SwiftUI, but the layout doesn't always match up with mobile Safari, so be sure to test on device.

To debug your extension's JS scripts, you'll need to use Safari on your mac. Open Safari, enable the develop menu , then open the develop menu in the menu bar. Select whatever device you're running on, and select the relevant page, which will bring up a web inspector.

safari extension swift tutorial

To my knowledge, there's no way to automatically open a web inspector when you open your popup page. I wish this were the case, since I spent a lot of time manually re-opening web inspectors. Also, every time you re-run your Xcode scheme, your breakpoints get cleared, which sucks. The lack of tooling here is what makes developing Safari Extensions a pain.

So you've tested your extension and its ready to go, now you just need to archive your project and upload it to App Store Connect. There's one small step that you might forget, which is to build your extension source for prod instead of dev mode. This dramatically reduces file size (in my case from 3.6 MB to 240 KB). In dev mode, Webpack includes a source map of your original, unbuilt code so you can debug more easily. If you've looked at your popup.js file inside your dist/ folder, it's a giant mess of unreadable code. Building for prod removes these source maps and minifies your code for production.

You can manually run " npm run build " to build for prod before you archive, or you can set up a script to do it automatically. I prefer the latter. Here's how to do it:

Edit your main app's scheme (not the extension scheme). Then, under Archive->Pre-actions , add a new Run Script Action.

safari extension swift tutorial

Write your shell script. Make sure that "Provide build settings from" is set to your app target. There are a few things to note about the actual script:

I used the say command because I wasn't sure the script was actually running, so it helped debug.

Pre-actions aren't logged in the build log, so the second line redirects all output from this script to a file called prearchive.log in my main project directory.

Replace the path in line 4 with whatever path your extension is located at.

safari extension swift tutorial

Now every time you archive your app, this script will run and automatically build your extension for production.

Other Challenges

iOS takes memory management seriously. As of iOS 15.0, the memory limit for a Safari Extension is 6 MB. This isn't documented anywhere to my knowledge, but you will discover it once your app is randomly terminated, as I did. This limit only refers swift code in your extension target, not JS/html/css. This seems like an incredibly unfair limit, given that running the template app in Xcode uses 3.8 MB (over 50% of the limit), and most of that is just Apple frameworks. According to one Apple employee on the Apple Dev forum , they're increasing the limit to 80 MB in iOS 15.1. If that's true, it's a 13x jump, which makes me think someone totally miscalculated and it somehow wasn't caught during beta. [Only real devices have this limit, simulators don't.]

safari extension swift tutorial

Even if the limit is increased to 80 MB, I would try to use the SafariWebExtensionHandler as little as possible and do most of your work in the JS; the browser does not have the same strict memory limits. In my case, I needed to use native messaging to get the user's auth state from the shared keychain. Other than that, I used node modules and JS fetch requests to get all the data I needed.

It's trivial to ignore the safe area in a native app, but what about in Safari? Turns out Safari uses css environment variables to accomplish this. The only problem was that they didn't work for me. I tried a bunch of different things, like setting viewport-fit in a meta tag , but no matter what, the variables all evaluated to 0px. Maybe I was doing something wrong, but this whole approach is such a mess.

Other notes

In the documentation, Apple specifies to use the browser API namespace, but it seems that chrome APIs work just fine. Why?

Final thoughts

Many people say that the browser is a second-class citizen on iOS, and after developing a Safari extension, I understand why. There's a major lack of tooling here compared to native development. Granted, Safari Extensions are only a few months old.

Apple's strategy has generally been to push towards its native App Store, where it has much tighter control and can extract more value. The browser, on the other hand, is the wild west. For a while people thought that PWAs would be the next big thing, since they work cross-platform out of the box and are easier to deploy and maintain. But that never happened, since the UX of a PWA is still significantly worse than a native app. That must be at least somewhat intentional on Apple's part. Maybe they'll find a way to levy a 30% tax on all payments in Safari, too.

Anyway, the market for browser extensions has never been that big. Compared to the market for mobile apps, it's tiny. Browser extensions tend to be more like utilities (e.g. a password manager, ad blocker, theme provider), rather than full-blown applications. They're called "extensions" for a reason - they're meant to incrementally extend or enhance your browsing experience. The one notable winner here is Honey, a browser extension that got acquired for $4 billion (!) in 2019. I think there are a lot fewer opportunities to build standalone businesses around browser extensions, but they can make a great feature to complement your other apps. That's the approach I took with my app, hbd . (If you've made it to this far, please check out the hbd Safari extension!)

Anytime Apple introduces a user-facing framework like this, it's worth paying attention to. That's the beauty of developing for iOS - every year there are 3 or 4 totally new APIs that affect 1B people. I'm not super bullish on Safari extensions since I don't see any big opportunities, but I wouldn't be surprised to see an emergent viral Safari extension soon. So far, popular Safari extensions look a lot like regular chrome extensions, just ported to Safari. What can you do on iOS that you can't on desktop? What's different about how people use Safari on iOS? Whoever cleverly answers those questions will have a big opportunity on their hands.

If you have any comments, corrections, or improvements to my process, reach out to me at [email protected] and I'll be happy to update this post.

Recent Posts

The Curious Design of Skateboard Trucks

The Smartest Website You Haven't Heard of

Will Two Cars Crash at an N-Way Stop Sign?

IMAGES

  1. How to launch a macOS SwiftUI app from a Safari extension

    safari extension swift tutorial

  2. HOW TO OPEN LINKS IN SAFARI || USING SAFARI WEB VIEW IN SWIFT 3

    safari extension swift tutorial

  3. Swift Tutorial For Beginners

    safari extension swift tutorial

  4. How to Install, Manage, and Delete Safari Extensions

    safari extension swift tutorial

  5. How to add Safari Extensions in iOS 15 on iPhone and iPad

    safari extension swift tutorial

  6. How to Create a Safari Extension from Scratch

    safari extension swift tutorial

VIDEO

  1. Add Safari Extensions on iPhone

  2. How to Use the Recipe Gallery Safari Extension

  3. How to Check Safari Version in Macbook [easy]

  4. Maasai Mara

  5. Swift Sight Safari: Dash through Optical Enchantment

  6. Must Have Safari Extensions for iPhone I Best Safari Extension for Reading on iPhone

COMMENTS

  1. Building a Safari app extension

    To create a Safari app extension: Launch Xcode and either open an existing project containing a macOS app or create a new one. Choose File > New > Target. From the list of templates in the Application Extension section, select Safari Extension, and click Next. Enter a product name for your extension, such as My Extension.

  2. How to create and use extensions

    Having to call trimmingCharacters(in:) every time is a bit wordy, so let's write an extension to make it shorter: extension String { func trimmed() -> String { self.trimmingCharacters(in: .whitespacesAndNewlines) } } Let's break that down…. We start with the extension keyword, which tells Swift we want to add functionality to an existing ...

  3. Creating a browser extension for Safari and Chrome

    The container Swift project for Safari Another issue is how to manage the React/extension project with the container Swift project. The boilerplate extension resources (JavaScript/css, manifest and html files) created with a new Safari extension project are managed by Xcode.

  4. How to Build a Safari App Extension in iOS 15

    If you already have an Xcode project, go to File->New->Target, and select Safari Extension (NOT Safari Extension App). Xcode will add a few boilerplate files in a new group: SafariWebExtensionHandler.swift is the glue between your native app and the JS code. It implements the NSExtensionRequestHandling protocol, which allows it to receive and ...

  5. swift

    In this case, you'll need to use an absolute URL that begins with the extension's base URL. To get the base URL, use safari.extension.baseURI. Here's the 6th line from the above example but using such an absolute URL: oReq.open('GET', safari.extension.baseURI + 'myfile.html'); A more hacky alternative to using XMLHttpRequest is to load your ...

  6. Creating a Safari App Extension

    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.

  7. How-to Build an iOS Share Extension in Swift

    Initial Steps. Either create a new project, or open an existing one you'd like to add the extension to. Then hit File > New > Target. Under the iOS tab select Share Extension and press Next ...

  8. How to open web links in Safari

    Learn Swift coding for iOS with these free tutorials. Forums. Learn. ... SwiftUI gives us a dedicated Link view that looks like a button but opens a URL in Safari when pressed. It's easy enough to use - just give it a title for the button, plus a destination URL to show, like this: ... TAKE YOUR SKILLS TO THE NEXT LEVEL If you like Hacking ...

  9. How to build an iOS Safari Web Extension

    But you actually want swipe out of the native app and go "Home" on your simulator or device and go to the Safari app. Tap the A A to open the Safari menu and toggle your extension on. Now when ...

  10. When should you use extensions in Swift?

    Updated for Xcode 15. Extensions let us add functionality to classes, structs, and more, which is helpful for modifying types we don't own - types that were written by Apple or someone else, for example. Methods added using extensions are indistinguishable from methods that were originally part of the type, but there is a difference for ...

  11. Beginner's Guide to Safari App Extension Development

    How to get started developing a Safari App Extension in Xcode. Understanding the file structure of the template app extension and getting a basic extension r...

  12. How to create a Safari extension

    Step 3: Create the Extension. From the Develop menu, choose Show Extension Builder. Click the Plus button in the bottom left corner of the window and choose New Extension. In the Save dialog box ...

  13. Learning SwiftUI

    Discover tips and techniques for building multiplatform apps with this set of conceptual articles and sample code.

  14. Setting up

    Learn Swift coding for iOS with these free tutorials. Forums. Learn. ... Learn advanced Swift and SwiftUI on Hacking with Swift+! >> < Overview : ... Setting up. In this project you're going to create a Safari extension, which lets us embed a version of our app directly inside Safari's action menu, then manipulate Safari data in interesting ways.

  15. Summary: Protocols and extensions

    Extensions let us add functionality to our own custom types, or to Swift's built-in types. This might mean adding a method, but we can also add computed properties. Protocol extensions let us add functionality to many types all at once - we can add properties and methods to a protocol, and all conforming types get access to them. When we ...

  16. Safari Extensions

    Web extensions. Extend the web-browsing experience by allowing Safari to read and modify web page content. Now supported in iOS 15 and iPadOS 15, Safari web extensions are available on all Apple devices that support Safari. These extensions are built with Xcode and can communicate and share data with native apps — so you can integrate app ...

  17. The Ultimate Guide to WKWebView

    1. Making a web view fill the screen. Sometimes you'll see folks add code to viewDidLoad() to create a web view then make it fill all available space. This is inefficient, and also far harder than it needs to be. A simpler approach is to add a property to your view controller like this: let webView = WKWebView() Then overriding the loadView ...

  18. Extensions

    extension Int { func squared() -> Int { return self * self } } To try that out, just create an integer and you'll see it now has a squared() method: let number = 8. number.squared() Swift doesn't let you add stored properties in extensions, so you must use computed properties instead. For example, we could add a new isEven computed property ...