Chapter 9. Extensions

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.

9.1 Creating Safari Content Blockers

Problem

You want to create a content blocker that the user can add to her Safari browser for blocking specific web content.

Solution

Use the Safari Content Blocker extension.

Discussion

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).

Figure 9-1. Adding a new Content Blocker extension to our existing app
Note

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
      print("Failed to reload the blocker")
      return
    }
    print("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).

Figure 9-2. Searching for blocker will allow you to go directly to the Content Blockers settings section of Safari

Once there, you can enable or disable available Safari content blockers (see Figure 9-3).

Figure 9-3. Our app is shown in the list of content blockers as the only available application as of now

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"]
  }
}
Note

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"]
  }
}

9.3 Maintaining Your App’s Indexed Content

Problem

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.

Note

This is an extension to the search capability explained in Recipe 10.1.

Solution

Add a Spotlight Index Extension to your app and update the index right in your extension (see Figure 9-8).

Figure 9-8. Adding a Spotlight Index Extension will allow us to reindex our app’s searchable content

Discussion

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.

Note

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()
}
            
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset