App Frameworks • iOS, macOS • 35:48
Learn how to bring your Content Blocker App Extensions to macOS and how to expose your application's abilities through Safari with Safari App Extensions. Safari App Extensions let you take full advantage of web technologies, Cocoa, and other system frameworks that you already use in your app on macOS.
Speakers: Brian Weinstein, Damian Kaleta, Zach Li
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript has potential transcription errors. We are working on an improved version.
Hello, everyone. I'm Brian Weinstein. I'm an engineer on the Safari team. And today Zach, Damian and I are here to show you how to extend your Mac app using Safari app extensions. In macOS Sierra and Safari 10 on El Capitan, we are introducing a new way of writing, packaging and distributing Safari extensions.
These extensions leverage both web technologies and native code written in Swift. They're bundled with the Mac app and can be distributed through the App Store. That new way is Safari app extensions. Safari app extensions have the power to customize the contents of webpages using JavaScript and CSS. They have the power to block loading of specific resources or elements on a page.
They can add toolbar buttons to Safari's UI. And those toolbar buttons have the power to display popovers using fully native technologies to build the views. And lastly, Safari app extensions can add items to context menus on webpages. Safari app extensions are based on app extensions. Which are bundles of code and resources that are packaged inside of your app that are meant to give your users access to your app's functionality and content throughout macOS.
And so this means that they are developed using Xcode and other developing tools you might already be familiar with and it means that Safari app extensions can run native code with the power of the APIs available to Mac Apps. Another major benefit about these being based on app extensions is that they are distributed with your app.
Which means that they can be sold through the Mac App Store. And it means that your users don't need to download your extensions separately after installing your app. And for those of you who require communication between your app and your extension, a major benefit of Safari app extensions is that your extension is always revlocked with your app because they're packaged together. So, your users will always have matching versions of your app and your extension.
One of the best parts of an extension platform is that it's capable of building many different types of extensions. Today we're going to build three types of extensions. The first type is a content blocker which blocks, which can block the loading of specific resources or hide elements on the page.
The second type will be an extension that modifies the contents of webpages and communicates between the JavaScript code provided by your app extension and the extension's native code. And the third type of extension will show you how to extend Safari's UI to add the power and functionality of your app directly to Safari. So, let's get started with content blockers. To build a content blocker for us, I'd like to bring Zach on stage. Zach?
[ Applause ]
Thanks, Ryan. Hi. I'm Zach Li and I'm also an engineer on the Safari team. Last year in Safari we introduced a new Content Blocking model. Rather than developing extension code on runtime to block loads or hide elements, we built a model where you can declare ahead of time what content to block.
And WebKit optimizes such blocking mechanism to make it fast and memory efficient. Best of all, this model preserves user privacy because the host app isn't consulted for the resources block. Since then, we've seen so many awesome content blockers you have written that really enhance the browsing experience. And users love them.
Today, I'm excited to talk about how to easily bring out existing iOS content blocker to macOS. For those of you who haven't written a content blocker before, that's no problem. You can check out the WWDC talk last year on developer.Apple.com. Let's take a look at what a content blocker can do. I really love eating. I basically eat all the time. Obviously I have a food blog that's dedicated to the food I want to try.
But unfortunately, well, actually fortunately, I will attend my friend's wedding as his best man next month. That I need to stay fit. At least I need to be able to fit into the suit. So, I created this iOS content blocker that blocks all the desserts from my website, that even if I'm starving and I look for something on my food blog, I wouldn't think of desserts at all. Let me show you how the dessert blocker works.
So, this is my source code and my app dessert blocker is already running. Let's go to Safari. And the desserts are present. Let's reload the page with the dessert blocker enabled. All the desserts are gone. That's great. Although I'll be missing them. So, I am an iOS developer with this content blocker and I really want to bring it to the Mac and distribute it through the App Store. It's actually quite simple to do because the content blocker APIs are the same on both platforms. So, let's first go to Xcode. Create a new Mac Application target that's called dessert blocker for Mac.
And let's quickly switch the Mac Applications theme, because we are now building a Mac Application. Then, create a new Mac Application extension target. Use the content blocker extension template. That's called dessert blocker extension for Mac. When users get Apps from the App Store, they don't have to run the application in order for Safari to find the app extension. But outside the App Store, Apps have to be run in order for Safari to see the extension. So, in this case, I actually want to continue using the application scheme. So, I'm just going to click on cancel.
This template will set everything up correctly. And it comes with a simple content blocking rule. To make your own content blocker, you just need to modify this JSON file. It's really convenient. But in our case, we can actually share resources with iOS app. So, we can get rid of this JSON file. And the Swift file. And make the ones we already have be available to the Mac Application, Mac app extension target. To accomplish that, go to utility sidebar. And in the target membership section, check the Mac app extension target for the resources we want to share.
Cool. We're pretty much good to go. But before that, I do want to make my dessert blocker more polished by displaying a better name and a better description in Safari extensions preferences. To accomplish that, let's go to the app extension info list. Instead of dessert blocker extension for Mac, let's check, just called it dessert blocker.
And the description I want to provide is this content blocker blocks dessert pictures on my food blog.com. Also, I want to provide prettier and more vivid icons for my dessert blocker. Let's remove the default Asset catalog. And drag the ones with predesigned ice cream icons to my Xcode project. All right. Let's view it and run the application for Mac and the application is run. So, let's go to Safari.
As you can see, it shows up in extensions preferences. Like all other Safari extensions, content blocker is off by default. Let's enable this dessert blocker. Reload the page. Great. All the desserts are gone. Awesome. Now that my urges to eat desserts are completely blocked, I'm so ready to be the best man.
[ Applause ]
As you saw, without even writing any code we ported an iOS content blocker to macOS. On top of that, we've heard feedback from you that it would be nice to know whether your content blocker is enabled. So, we are introducing a new API, getStateOfContent Blocker(identifier. With this API, for example, if you recall my not so great UI in the app, I can provide a better experience to users by giving instructions on how to enable my content blocker if I detect my content blocker is not enabled. This API along with other APIs that my colleague Brian and Damian are going to talk about will be available on Sierra and El Capitan if users have installed Safari 10.
Because El Capitan, the API availability is dependent on the presence of Safari 10. We are providing you with a handy Swift API that you can use to check at runtime whether Safari services APIs whether Safari services APIs are available. Let's step through the Swift code API. If SFSafari service is available returns true, you can safely use the new API. If not, fall back to other behaviors. These are what's new with content blockers. To talk about other powerful capabilities you can view Safari app extensions, I would like to hand the stage back to Brian.
[ Applause ]
Thanks, Zach. So, the next type of extension I would like to show is one that modifies the contents of a page and also has the ability to communicate with the native code provided by your app extension. So, we're going to build an extension that replaces instances of one word with another throughout the web.
It will get the word to replace and what to replace it with from a app extension's Swift code. There are 2 ways for Safari app extension to modify the contents of the webpage. extensions can inject CSS style sheets and JavaScript content scripts into pages. Let's take a look at how to inject a style sheet.
Stylesheets are injected by specifying them in the app extensions Info.plist in the NSExtension section. Stylesheets are specified using the SFSafari style sheet section of the Info.plist file which inspects an array of dictionaries defining each style sheet. And each style sheet is defined by a style sheet key-value pair where the value is the path to the style sheet relative to the resources directory of the extensions bundle.
And any style sheet you've written in the past for a Safari extension will just work with Safari app extensions. Next up is injecting scripts. Which is almost exactly the same, just with two different keys. Scripts are specified in the SFSafari content script key of the dictionary. And the key representing the path is script instead of style sheet.
And all of your extensions scripts are run in their own execution context which means there are never there are never naming conflicts with variables in webpages content scripts. This also allows Safari to provide special JavaScript APIs to your extension's content scripts that aren't available to all webpages. For example, your extension might need to communicate with the native code in your app extension to read preferences or perform some action that can only be done in native code. And to facilitate this, we've added a message passing API so your app extension's JavaScript code and Swift code can communicate with each other.
So, taking a look at this diagram, you can see that we have Safari and we have your app extension with a box representing the Swift code running in your extension's process which is of course completely sandboxed. And the extension has injected a script into Apple.com and that script would like to ask that app extension what words it should replace.
To do that, it just calls Safari.extension.dispatch message and passes along a message name. Now let's see how the Swift code listens for this message and responds back with the words it should replace. Each Safari app extension has a principal class which is the class that Safari calls methods on when the user interacts with Safari. When a message is dispatched from a content script to the app extension, message received with name from page userInfo is called.
And once that method is called on our extension's principal object, the first thing we do is check the message name and act on it. And to act on it, our extension will be sending back a message to the SFSafari page representing Apple.com with the words to replace and what to replace them with.
And we are structuring this at the dictionary where each key represents the word that we're going to replace and the value represents what we're going to replace it with. So, when the app extension calls dispatchMessageTo Script(withName,userInfo, that message is sent from the extension process back to the content script that is injected into Apple.com. Now let's see how that content script listens for these messages and acts on them.
To listen for a message inside a content script, we start by adding an event listener to Safari.self. So, the content script can listen for message events from the app extension. And if you've written a Safari extension before, this will look very familiar. The API to receive messages inside of a content script is almost exactly the same.
And when our event listener fires because we got a message from our app extension, the first thing we do once again is check the message name as a best practice and then we act on it. To act on this message, we want to iterate over our dictionary that was specified in the userInfo of the message from the Swift code and that is exposed to JavaScript as event.message. So, we get our dictionary of words and replacements and we iterate over them and call a function in our content script that does those replacements.
So, this word replacement extension that we've been discussing is meant to work on every website. But some extensions are tailored to only run on particular websites. Safari app extensions support a rich system for specifying what sites they work on and additionally new in Safari 10, users will be able to see which sites your Safari extension requires access to.
Like information about content scripts and style sheets, your extension's website access is described in the extension's Info.plist under the SFSafariWebsiteAccess/key and it's a dictionary with two parts. The first part is the website access level. And this extension is specifying an access level of all, which means it wants access to every webpage that the user visits. And as you can see, Safari's extensions preferences warn the user about this before they're going to turn on the extension.
And in this example, the extension has an access level of some. When the access level is some, the extension specifies a list of domains that it wants access to in the allowed domain section of the dictionary. And if an allowed domain starts with an asterisk, that marks it as a wildcard and it represents access to all subdomains.
So, now that we've discussed how your extension can modify pages by injecting content scripts and style sheets, how those content scripts can communicate with your app extension's Swift code and how you specify what websites your extension would like access to. I'd like to put this all together in a demo and write that word replacement extension that we've been talking about.
So, one of the interesting things about Safari extensions is that since the code that interacts with webpages is JavaScript based, it's very easy to bring code that you've written for, an extension for a different browser, and bring it directly into Safari. So, what I'm going to do is I'm going to take a Chrome extension that does this word replacement and I'm going to create a Safari app extension from it. And I'm going to extend it so that we get the words to replace and what to replace them with from Swift code.
So, I already have an app built with an icon and I would like to create a Safari extension. To do that, I create a new target in Xcode and select a macOS Application Extension and select Safari extension. And we're going to call this Animalify because we're going to Animalify the web by replacing animals with their emoji. And as Zach mentioned before, we want to run the app so I'm not going to activate the extension scheme right now.
So, we now have our extension and I'm going to jump into the Info.plist. And open up that NSextension section. And as you can see, automatically created for us from the template, we have a content script, a toolbar item and the website access that our extension requires. I'm going to get rid of this toolbar item because our extension doesn't need it.
And change the access level to all and get rid of the list of allowed domains because we want this extension to run on every page. The next thing I'm going to do is open up our content script and I'm just going to bring in the content script I've written for my Chrome extension wholesale.
And as you can see, when the script is injected into the page, it just calls our replacement function and replaces bear with the bear emoji. Now, what I would like to have my app do is I would like to have it replaced, I would like to have it show a list of animals and the users can choose which animals they would like to replace. I'm not going to design any of that UI right now but to support that, we need to get the list of replacements from the app extension.
So, instead of doing this replacement straightaway, we're going to send a JavaScript message to the app extension asking it for the words and replacements. And so one thing that's interesting here is that Safari's extensions, Safari app extension content scripts are injected before the dom has loaded for more flexibility in your extensions. But since a word replacer doesn't do much before there's a dom, we want to wait until the dom is loaded before sending this message.
So, now let's go to our principal class and as you can see, we already have an implementation for message received with name from page userInfo. So, we're going to get rid of this and just replace it with some, with the code that we talked about in the slides. Which all it does is check the message name and sends a response back to the content script. And we're making two replacements here just because we can. So, let's go back to the content script and listen for this message and act on it.
And once again, this is just the code we talked about a few minutes ago. We start by adding an event listener for the message event to Safari.self. And when that event listener fires, we check the message name and get our replacements from event.message. And we act on them by iterating over all of the replacements and calling that same replace function that was at the beginning of the script before. So, now I would like to build and run this app so Safari to discover this new extension.
And you can just imagine a list of animals here and I've checked a few. So, I'm just trying out Safari app extensions for the first time. So, I have not signed up for the app, I have not signed up for the Apple Developer Program yet which means I don't have a Developer certificate.
By default, Safari will only show, allow users to enable extensions that have been signed with the developer certificate. But for those of you who just want to try this out, we've added way for you to be able to test your unsigned extensions. To do that, I'm going to open up the Advanced Preferences and show the Develop menu in the menu bar.
And from the Develop menu, I'm going to select Allow Unsigned extensions. I'm going to type my password and now the animal extension shows up in our list of extensions. I'm going to turn it on and before everyone got here, I was researching the diet of a grizzly bear. So I'm just going to reload this page and as you can see, bear has now been placed, replaced with the bear emoji and salmon has been replaced with a delicious sushi emoji.
[ Applause ]
And this is just going to make the web a lot more fun for me to browse. So, that was how a Safari app extension can modify page content and how your extension specifies what websites it would like access to. The last type of extension that we're going to show you how to make is one that extends Safari's UI to add the power and functionality of your native app directly into Safari. And to show us how to do that, I'd like to invite Damian on stage. Damian?
[ Applause ]
Hello, everyone. My name is Damian Kaleta and I'm an engineer on the Safari team. So, Brian already told you about the fundamentals of Safari App Extensions and now I want to build on top of that. I want to tell you how you can extend Safari's UI. So, let's start. I've written this simple macOS app. It's a notebook app. And as you can see by the screenshot, the icon got some love from the designers. Unfortunately, the app itself didn't. But nevertheless, it lets me insert some note, saves it so I can view it later.
But now I want to be able to grab notes off of web pages directly and modify this note in Safari. And with the new Safari app extension model, I have all the tools I need in order to build my extension really easily. I'm going to need two different things. So, I want to be able to select a text and I want to be able to save it.
For that, I'll use a context menu item. Secondly, I want to be able to display my most recent note and I want to be able to modify it. For that, I'll use a popover. The popover shows up when the user clicks the toolbar button. So, let's talk about that first.
The toolbar button goes by default next to the Smart Search field. That way your users have really quick and easy access to the functionality that your extension provides. As you would expect, if you're an Events user of course you can move it around really easily. So, how did I add my button? I went to my extension's Info.plist and I've added SFSafariToolbarItem. Together with four different key-value pairs. Identifier, label, image and action.
And as with all toolbar items on the system, please notice that I'm using a PDF file here. Okay, so Safari now displays my toolbar button but what happens to my extension when the user clicks that toolbar button? Safari sends toolbar item click in window to your principal object.
And as a reminder, your principal object is the object that handles all of the communication between Safari and your extension. And also if you want to gray out the button depending on the context, Safari provides you with the validation method. And you can also set batch text for your button. That usually represents a numerical value such as in red count.
So, we've got ourselves a button but now I want to be able to display a popover when the user clicks the button. The popover lets me insert any NSview inside of it which is great because if you have written any macOS Apps before, you're going to be able to reuse that code really easily right over here. So, let me show you how this works.
You have your extension with the principal object and then you'll want to define popoverViewController method on your principal object. Inside that method, you'll want to return to custom view controller it represents a view that you want to insert in that popover. On the other side, there's Safari process and Safari process is something that we call remote view.
That remote view will simply grab your review and display its contents in the popover. And as you would expect, we will also forward you all of the events such as click events. And a way to show the popover is to simply specify an action of popover instead of command on SFSafariToolbarItem. And that's it. At this point, Safari knows that you want to display a popover, so you will look for that custom view controller.
The popover also comes with a handful of useful APIs. And as you can see here, the first two popoverWillShow and the popoverWillClose. It can help you do some setup or cleanup work. And the third one we've talked about. It's the one that basically returns your custom view controller.
Okay, so we've added the button. We can display a popover. Now let's talk about context menu items. So, you typically want to use context menu items when you want to act on part of the page. But in my case, I want to be able to select the text and then save it. So, I went again to my extensions Info.plist and then I've added SFSafariContextMenu. It's an array of dictionaries that hold two different key-value pairs. Text and command. And then when the user presses or clicks your context menu item, Safari will send contextMenuItemSelected to the principal object.
Please notice that we are also passing along userInfo. This will simply represent any additional information you might want to include from your injected script. Such as in my case, I want to be able to pass along selectedText. So, inside my injected script I'm adding event listener for context menu. And then inside that function I'm calling set context menu event userInfo on the Safari extension object. And notice that I'm actually sending along selectedText. Okay. So now I'm excited to show you my macOS app how I extended it into Safari. So, let's do that.
So, before I show you any code, this is my simple macOS app right over here. As you can see, I have just two notes. I can insert my note here, delete last note, etc. So, let's go to Xcode. I want to show you three different things. First, Info.plist.
So, please notice that I'm adding my context menu item right over here. Here's my text and my command. And I'm also adding my toolbar item. I have my, all of my four different fields right over here. And as you can see, I'm using a PDF file. Second thing I want to show you is my principal object.
Let me just make it like that. And I have overridden two different methods on my principal object. The first one is popoverViewController. This will simply return my view controller that represents the view that goes to, that displaced in the popover. And the second one is contextMenuItem Selected(withCommand. And as you can see, I'm getting my userInfo right over here and assigning my note here and here. And third thing I want to show you is how I can easily reuse the code and share it between my macOS app and my extension.
Of course normally you wouldn't use just simple files. You would use a framework but that will give an idea. So, this is my notes manager. It will simply read and save to the user defaults. It has some simple methods such as removeAllNotes, removeLastNote, etc. And please notice that my target membership is set for both targets, my notebook, which is macOS app and my extension.
Okay. So, now let me go to Safari right over here and as you can see, my extension is right next to the Smart Search field. I can click it and this is where my last, my most recent note will appear. So, let's say and, let me just bring up my notebook app also. Side to side. So, let's say I'm reading some blog posts and let's pick this one. And let's say that I want to be able to save some notes. So, let's say I select this thing.
I command, CTL click it. And then I say add snippet to notebook. Like so. As you can see, my note was added right over here also. I can open my extension and it's right over here. And now if I want to let's say modify it, because let's say I don't want to have my last sentence, I can simply delete that, hit Modify and voila. It's reflected in my macOS app.
So, it's that simple to create an extension and share the code between your macOS app and extension. So, I showed you how to add a button. I showed you how to display a popover and add multiple context menu items. Now I want to hand it back to Brian.
[ Applause ]
Thanks, Damian. Except for leaving the clicker over here. So, today we showed you three types of extensions that are made possible with our new extension model. We showed how easy it is to bring your content blocker from iOS to the Mac and introduced new APIs to get the state of your content blocker in response to your feedback. We saw how simple it is to bring a Chrome extension that modifies webpages to Safari and enhanced it to communicate with the native Swift code in your app extension. And lastly, we saw how it had the power and functionality of your app directly to Safari.
And remember, Safari app extensions are all based on app extensions. Which means you have the power of native technologies and APIs in your app extension alongside your JavaScript and your CSS used to modify and enhance webpages. And since Safari app extensions are distributed with your Mac app, there's an easier installation experience for your users and you can now sell your extensions in the Mac App Store.
For more information about the talk and some useful links, please visit this page. Safari app extensions are the future and we need your help to make them the best that they can be. Please give us some feedback about our APIs and any bugs you find. On the More Information page, you will find a link to leave that feedback and the email address of John Davis, our Safari and WebKit evangelist. And for related sessions, I highly recommend checking out some of the app extension talks from previous years. Thank you so much.
[ Applause ]