Apple increased the number of extensions that we developers can write in the new iOS. One of the hot extensions that everybody seems to be talking about is the Safari Content Blocker, which allows developers to specify which URLs or resources should get blocked in Safari tabs.
Extensions are separate binaries that sit inside your app’s bundle. They usually have their own naming convention and sit inside reserved folders inside your app bundle. It’s best not to mention what they are called on disk because Apple can change that at any time without us knowing. Because extensions sit in their own folders and have their own bundles, they do not share the same physical space as their container app. But, through some work, they can access the container app’s resources such as images and text.
Use the Safari Content Blocker extension.
This is something I am very excited about. You can ignore the long list of content blockers popping up on App Store every day from now on.
This is how the Apple blocker works. When you create an app, you can add a Safari Content Blocker extension to it. In that extension, you define the rules for your content blocking (whether you want to block images, style sheets, etc.). The user can then, after opening your app at least once, go into the settings on her device and enable your content blocker. From now on, if she visits a web page that your content blocker applies to, she will see only the content that passes your filters.
Let’s create a simple single view controller app and then add a new target to your app. From the iOS main section, choose Application Extension and then Content Blocker Extension (see Figure 9-1).
Give any name that you want to your extension. It doesn’t really matter so much for this exercise.
Now go to the new extension’s new file called blockerList.json and place the following content in it:
[
{
"action"
:
{
"type"
:
"block"
},
"trigger"
:
{
"url-filter"
:
".*"
,
"resource-type"
:
[
"image"
],
"if-domain"
:
[
"edition.cnn.com"
]
}
}
]
Even though there is a specific type of formatting to this file, I think you can just read this as I’ve written it and understand what it is doing. It is blocking all images that are under the edition.cnn.com domain name. Now head to your app delegate and import the SafariServices
framework. Every time you change your content blocker, you will have to go to the Settings application on the simulator and turn it off and on again so that the simulator understands that the extension is updated. We are now going to write a piece of code that automates that for us:
func
applicationDidBecomeActive
(
_
application
:
UIApplication
)
{
//
TODO:
replace this with your own content blocker's identifier
let
id
=
"se.pixolity.Creating-Safari-Content-Blockers.Image-Blocker"
SFContentBlockerManager
.
reloadContentBlocker
(
withIdentifier
:
id
)
{
error
in
guard
error
==
nil
else
{
// an error happened, handle it
(
"Failed to reload the blocker"
)
return
}
(
"Reloaded the blocker"
)
}
}
Then reset your simulator and run your app. Send your app to the background, open Safari on the simulator, and type in cnn.com
. This will redirect you to http://edition.cnn.com/ (at the time of this writing). Safari will hit the filter we wrote and discard all the images. The results will be lovely. (Well, I don’t know whether a website without images is lovely or not, but it’s what we set out to do.)
A user can always enable or disable a content blocker. To do that, you can go to the Settings app on your device and in the search field type in blocker
. Then tap the Content Blockers item that pops up (see Figure 9-2).
Once there, you can enable or disable available Safari content blockers (see Figure 9-3).
Now that you have seen an example, let me bug you with some more details on that JSON file. That file contains an array of dictionaries with various configurations that you can enter. This book would grow very large if I thoroughly described everything there, so I will simply explain the options for each field through some pseudo-JSON code:
[
{
"action"
:
{
"type"
:
"block"
|
"block-cookies"
|
"css-display-none"
,
"selector"
:
This
is
a
CSS
selector
that
the
action
will
be
applied
to
},
"trigger"
:
{
"url-filter"
:
"this is a filter that will be applied on the WHOLE url"
,
"url-filter-is-case-sensitive"
:
same
as
url
-
filter
but
case
sensitive
,
"resource-type"
:
[
"image"
|
"style-sheet"
|
"script"
|
"font"
|
etc
],
"if-domain"
:
[
an
array
of
actual
domain
names
to
apply
filter
on
],
"unless-domain"
:
[
an
array
of
domain
names
to
exclude
from
filter
],
"load-type"
:
"first-party"
|
"third-party"
}
}
]
Armed with this knowledge, let’s do some more experiments. Let’s now block all a
tags in macrumors.com:
{
"action"
:
{
"type"
:
"css-display-none"
,
"selector"
:
"a"
},
"trigger"
:
{
"url-filter"
:
".*"
,
"if-domain"
:
[
"macrumors.com"
]
}
}
I have no affiliation with nor any hate toward MacRumors—I find that website quite informative, actually. Check it out for yourself. I am using this website as an example only, and I am not suggesting that content on that website is worthy of blocking.
Or how about removing the a
tag on top of the macrumors.com page that is an id
attribute equal to logo
?
{
"action"
:
{
"type"
:
"css-display-none"
,
"selector"
:
"a[id='logo']"
},
"trigger"
:
{
"url-filter"
:
".*"
,
"if-domain"
:
[
"macrumors.com"
]
}
}
Now let’s have a look at another example. Let’s start blocking all images on all websites except for reddit.com:
{
"action"
:
{
"type"
:
"block"
},
"trigger"
:
{
"url-filter"
:
".*"
,
"resource-type"
:
[
"image"
],
"unless-domain"
:
[
"reddit.com"
]
}
}
Or how about blocking all elements of type a
that have an href
attribute on Apple’s website?
{
"action"
:
{
"type"
:
"css-display-none"
,
"selector"
:
"a[href]"
},
"trigger"
:
{
"url-filter"
:
".*"
,
"if-domain"
:
[
"apple.com"
]
}
}
You want to know when iOS is about to delete your indexed items and you would like to be able to provide new content to the search index.
This is an extension to the search capability explained in Recipe 10.1.
Add a Spotlight Index Extension to your app and update the index right in your extension (see Figure 9-8).
Every now and then, iOS has to clean up the search index on a device. When this happens, apps that have provided searchable content will be given a chance to reindex their items. To get started, create a Spotlight index extension as shown in Figure 9-8. I’ve given mine the name of Reindex
. It’s up to you what you want to name your extension. Now you will get a class called IndexRequestHandler
in your extension. It offers two methods:
searchableIndex(_:reindexAllSearchableItemsWithAcknowledgementHandler:)
searchableIndex(_:reindexSearchableItemsWithIdentifiers:acknowledgementHandler:)
The first method gets called when you are asked to reindex all your previously indexed items. This can happen if the index is corrupted on the device and you are asked to reindex all of your content. The second method will be called on your extension if you have to index specific items with the given identifiers. You will be given a function called an acknowledgment handler to call when you are done indexing again.
In both of these methods, the first parameter that you are given is an index into which you have to index your items. Use that index instead of the default index.
Here is an example. Let’s define a protocol that dictates what indexable items have to look like:
protocol
Indexable
{
var
id
:
String
{
get
set
}
var
title
:
String
{
get
set
}
var
description
:
String
{
get
set
}
var
url
:
URL
?
{
get
set
}
var
thumbnail
:
UIImage
?
{
get
set
}
}
And then a structure that conforms to our protocol:
struct
Indexed
:
Indexable
{
// Indexable conformance
var
id
:
String
var
title
:
String
var
description
:
String
var
url
:
URL
?
var
thumbnail
:
UIImage
?
}
Later on we are going to go through an array of Indexed
instances, grab all the IDs, and put those in an array. Then, when we are asked by iOS to index certain items with given IDs, we can just find that ID in our array, and then find the associated indexed item using the ID. For this, we can use protocol extensions on sequence types. I wrote about this in Recipe 5.12:
extension
Sequence
where
Iterator
.
Element
:
Indexable
{
func
allIds
()
->
[
String
]{
var
ids
=
[
String
]()
for
(
_
,
v
)
in
self
.
enumerated
(){
ids
.
append
(
v
.
id
)
}
return
ids
}
}
And now the juicy part—our extension. We construct an array of indexed items:
lazy
var
indexedItems
:
[
Indexed
]
=
{
var
items
=
[
Indexed
]()
for
n
in
1.
..
10
{
items
.
append
(
Indexed
(
id
:
"id
(
n
)
"
,
title
:
"Item
(
n
)
"
,
description
:
"Description
(
n
)
"
,
url
:
nil
,
thumbnail
:
nil
))
}
return
items
}()
When we are asked to reindex all our items, we just go through this array and reindex them (see Recipe 10.1):
override
func
searchableIndex
(
_
searchableIndex
:
CSSearchableIndex
,
reindexAllSearchableItemsWithAcknowledgementHandler
acknowledgementHandler
:
@
escaping
()
->
Void
)
{
for
_
in
indexedItems
{
//
TODO:
you can index the item here.
}
// call this handler once you are done
acknowledgementHandler
()
}
When we are asked to reindex only specific items with given identifiers, we use our sequence type extension to find all the IDs of our indexed items. Then we search through these IDs for the IDs that iOS gave us. Should we find a match, we will reindex that item. Code for reindexing is not shown here, but Recipe 10.1 shows you how to do it:
override
func
searchableIndex
(
_
searchableIndex
:
CSSearchableIndex
,
reindexSearchableItemsWithIdentifiers
identifiers
:
[
String
],
acknowledgementHandler
:
@
escaping
()
->
Void
)
{
// get all the identifiers strings that we have
let
ourIds
=
indexedItems
.
allIds
()
// go through the items that we have and look for the given id
var
n
=
0
for
i
in
identifiers
{
if
let
index
=
ourIds
.
index
(
of
:
i
){
let
_
=
indexedItems
[
index
]
//
TODO:
reindex this item.
}
n
+=
1
}
acknowledgementHandler
()
}