Adding your app contents to the Spotlight index

If you have ever worked on a website, you must have heard something about search engine optimization. More importantly, you will know that any website you create and publish gets indexed. All you have to do is make sure that you write semantic and structured HTML markup and any web spider will understand what your website is about and what parts of it are more important. Search engines, such as Google, have indexed billions of web pages based on their contents and semantic markup.

Apps tend to be a little less neatly structured, and crawling them is a lot harder if not impossible. There is no structured way to figure out what content is on screen and what this content means. Also, more importantly, a lot of content you'd want to index is only available to users who have logged in or created a content of their own.

This is why Apple decided that the developers themselves probably know their app's contents best and should be in charge about how, when, and why a particular content is indexed. Even though this does put a little bit of manual burden on the developers, it gives them a huge advantage over the automatic indexing that's done on the web. Since developers are in control, they can decide exactly the content that matters most to specific users. As you'll soon see, you can index content based on the screens your user visits, which means that you will just index those pages that your user may want to visit again.

Even more important than being in control is the ability to safely index private contents. The web is limited to indexing public contents. If you use online e-mail software to check your inbox or if you have an online project management tool, you must rely on the internal search functions inside the web page for these tools. You won't find your e-mails or projects through a regular search query. With Spotlight indexing, your users can do just that, search through their own content. Indexing private contents is secure because the data is not made available to other apps and you can't accidentally make one user's private data visible to other users due to the public indexing threshold mentioned earlier.

So how exactly do we take control then? That's the question that will be answered next. We'll take a look at the following three different methods that Apple came up with to index app contents:

  • NSUserActivity
  • CSSearchableItem
  • Universal links

Most of the time will be devoted to the first two methods since the next chapter, Chapter 14, Making the Web and your App Meet through Universal Links, will focus on the third method. We will briefly discuss them and their role in Spotlight search, but an in-depth overview will be provided later.

Indexing your app through user activity

As part of a feature set called Continuity, Apple launched Handoff in iOS 8. Handoff allows users to start an activity on one device and then continue it on another. In iOS 9, Apple introduced the ability to index these user activities in Spotlight. There are several advantages to this because apps that support Handoff hardly need to do anything to support Spotlight indexing and vice versa. So, even though this chapter is all about search and Spotlight, you've already learned something about enabling Handoff for your app.

The philosophy behind user activities is that whenever your user does something with your app, you create an instance of NSUserActivity. For Spotlight, these activities revolve solely around viewing contents. Any time your user looks at a piece of content in your app is a good time to create a user activity and have Spotlight index it.

After the user activity is added to the index, the user will be able to find it through Spotlight, and when they tap on it, your app can take the user straight to the relevant section of the app.

In the previous chapters, we worked on an app called FamilyMovies. In this app, we collected data about family members and their favorite movies. We also had a rating for each of the movies. We will add Spotlight indexing to this app.

Since the last time you worked on this app, a few additions were made. The app now contains a tab bar. There is a tab for the family members list, and there is one that lists all of the movies that are added to the app. Selecting a movie will display a list of family members who have added this movie to their favorites.

An app such as this is a great candidate for indexing. We can add the separate family members, the tabs from the navigation bar and the movies to Spotlight to make them searchable from anywhere within the iOS. We'll start off simple; we'll index the Family Members and Movie tabs in Spotlight as the user visits them.

We will use user activities for this, so we should create an activity whenever our user opens one of the two tabs. At first sight, there isn't much use in repeatedly pushing the same activity over and over again if we know that we already pushed it once. The following are two of the available options we have for tracking the opening of a tab:

  • Pushing the activity in viewDidAppear
  • Pushing the activity in viewDidLoad

If we create the user activity in viewDidAppear, we push a user activity every time the user switches tabs or navigates back to our view controller from another view controller. Even though it doesn't cost much to index a user activity, it seems like it's somewhat overkill to index our tab every single time a user sees it.

However, as we'll see in the implementation soon, the best way to go about this is to actually place the activity creation in the viewDidAppear method. If we were to put this logic in viewDidLoad, it would only be executed once even though the idea of user activities is that they describe each activity a user performs.

In this case, repeatedly inserting the same activity over and over again is actually the desired behavior because it accurately reflects what the use is doing inside of your app.

Let's take a look at some code that indexes the family members tab; it should be added in the FamilyMembersViewController:

override func viewDidAppear(_ animated: Bool) { 
    super.viewDidAppear(animated) 
 
    let userActivity = NSUserActivity(activityType: "com.familymovies.openTab") 
    userActivity.title = "Family Members" 
    userActivity.isEligibleForSearch = true 
    userActivity.isEligibleForPublicIndexing = true 
     
    self.userActivity = userActivity 
    self.userActivity?.becomeCurrent() 
} 

The preceding code shows how to create a very simple user activity. The activity we just created only has a title because there isn't much involved in it. Note that we added a string that looks similar to the app's bundle identifier as the activityType parameter for the NSUserActivity initializer. This identifier will help us distinguish the intent for the activity later on.

The most important thing to note in this method of indexing content is the isEligibleForSearch property. This property tells the system that the user activity that we're about to set as the current activity can be indexed for searching. Other, similar, properties are isEligibleForHandoff and isEligibleForPublicIndexing. It's actually a great idea for our activity to make it eligible for public indexing, so go ahead and set that property to true. Doing this will make our activity show up in search results of a lot more people, if enough people interact with it. Making an activity eligible for handoff enables us to continue the activity on another device. Since our app only works on iOS and we don't really need to take multiple users with multiple devices into account, we don't have to set this property to true.

Finally, the user activity we just created is made the current activity. This makes sure that the OS registers our activity and adds it to the Spotlight index. It won't be made available publicly right away because, as discussed, there is a threshold of people, that interact with this activity, we need to reach before Apple will push it to all users.

If you build and run your application, the family members tab should be the first tab to appear. This means that a user activity for that view is to be created and indexed immediately. After opening the app, go ahead and open Spotlight by swiping down on the home screen and perform a search for family.

You'll notice that the activity we just added is listed under the FamilyMovies header in the Spotlight search results, as shown in the following screenshot:

Indexing your app through user activity

Pretty neat, right? We were able to add a simple entry in Spotlight's search index with a very minimal effort. You should be able to add a similar snippet to the app for the movies tab. Go ahead and add a modified version of the earlier snippet to MoviesListViewController.

Now that both tabs show up in Spotlight, how do you make sure that the correct tab is opened when the user selects a result from Spotlight? The answer is in one of the AppDelegate's methods. Whenever your app is brought to the foreground because a user selected your app as a Spotlight search result, the application(_:continueUserActivity:restorationHandler:) method is called.

This method receives the activity that we are supposed to resume, and it's up to our application to resume this activity as soon as possible. Apple actually has algorithms in place that measure how long it takes for your app to resume the activity to make sure that it can either reward or punish your app in Spotlight's rating system.

We'll discuss this a bit more when we get to best practices and ranking, but it's good to know that you should show the users what they're looking for as fast as you possibly can.

In our case, it's quite simple for us to resume the activities we passed. There are only two possible scenarios that we need to handle right now. Either the users want to look at the family members tab or they want to see the movies tab. Let's take a look at how we can implement this behavior in AppDelegate.

What we need to do in our implementation of application(_:continueUserActivity:restorationHandler:) is to inspect the user activity we passed to determine the tab we should display. Once we know this, we should obtain a reference to the UITabBar that holds our tabs and set its active view controller to the tab we need to select. Finally, we should bring the navigation controller that's displayed to its root. We need to do this as otherwise if our user was looking at a detail page before going into Spotlight, we would show them the detail page instead of the page they would expect to see. It would show them whatever the tab was previously displaying. Popping to the root ensures we show them the top level view.

func application(_ application: UIApplication, continue userActivity: 
  NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool { 
    guard let tabBar = window?.rootViewController as? UITabBarController 
        else { return false } 
     
    let tabIndex: Int? 
    if userActivity.title == "Family Members" { 
        tabIndex = 0 
    } else if userActivity.title == "Movies" { 
        tabIndex = 1 
    } else { 
        tabIndex = nil 
    } 
     
    guard let index = tabIndex 
        else { return false } 
     
    guard let navVC = tabBar.viewControllers?[index] as? UINavigationController 
        else { return false } 
     
    navVC.popToRootViewController(animated: false) 
    tabBar.selectedIndex = index 
     
    return true 
} 

In this basic implementation of application(_:continueUserActivity:restorationHandler:), we simply take care of the two cases we indexed. First, we make sure that we actually have a UITabBarController to work with in our app. If this isn't the case, we can't handle the activity and we return false. Next, we set the tabIndex if possible and extract the corresponding navigation controller from the tab bar controller. Again, if this fails, we can't handle the activity and we return false.

Finally, when all the requirements are in place, we pop the navigation controller to its root and set the selected index on the tab bar to the correct value for the activity we're looking to handle.

We're currently at a point where we managed to index the two main screens for the app. However, there are a couple more screens that we can index. This will make our implementation for application(_:continueUserActivity:restorationHandler:) much more complex.

Manually creating user activities for each screen in our app is tedious and involves quite a lot of boilerplate code. We'll solve this by utilizing an activity factory. A common pattern in apps is to use a specific helper object called a Factory. The sole purpose of a factory is to have a central place to create instances of a certain type. This greatly reduces boilerplate code and increases maintainability. Create a new file called IndexingFactory.swift in the Helpers folder and add the following implementation:

import Foundation 
 
struct IndexingFactory { 
    enum ActivityType: String { 
        case openTab = "com.familymovies.openTab" 
        case familyMemberDetailView = "com.familymovies.familyMemberDetailView" 
        case movieDetailView = "com.familymovies.movieDetailView" 
    } 
     
    static func activity(withType type: ActivityType, name: String, makePublic: 
      Bool) -> NSUserActivity { 
        let userActivity = NSUserActivity(activityType: type.rawValue) 
        userActivity.title = name 
        userActivity.isEligibleForSearch = true 
        userActivity.isEligibleForPublicIndexing = makePublic 
         
        return userActivity 
    } 
} 

An enum is used to abstract away the activity types we will use, and we have a single static method in the IndexingFactory struct. This method takes a couple of configuration arguments and uses these to create and return a new user activity instance. Let's take a look at a usage example for the family member details screen:

override func viewDidAppear(_ animated: Bool) { 
    super.viewDidAppear(animated) 
     
    guard let familyMemberName = familyMember?.name 
        else { return } 
     
    self.userActivity = IndexingFactory.activity(withType: 
      .familyMemberDetailView, name: familyMemberName, makePublic: false) 
     
    self.userActivity?.becomeCurrent() 
} 

The preceding implementation is a lot smaller than creating a user activity from scratch in every viewDidAppear. Also, if we decide to make changes to the way we create user activities, it will be easier to refactor our code because we can simply change a single method.

This wraps up simple indexing with NSUserActivity. Next up, we'll take a look at CSSearchableItem and how we can use this class to index content the user hasn't seen yet. You'll also see how you can associate more sophisticated data with your searchable items and how Spotlight handles updating and re-indexing of contents.

Tip

An exercise for you, the reader. We haven't implemented any code to handle user activity continuation to open details pages for family members and movies. We'll get to this soon, but it's a pretty good exercise to try and add this functionality on your own. If you get stuck, don't hesitate to take a look at the source code for this chapter because a full implementation can be found there. For a proper implementation, you'll need to add a new find method to Movie and FamilyMember and you'll have to instantiate view controllers straight from a storyboard.

..................Content has been hidden....................

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