Hacking with Swift — Learning Project 32
Today’s project is about adding our app’s content to Spotlight in iOS and take advantage of Safari integration.
Create a new Single View App Xcode Project, call it “Swift Searcher” and save it somewhere sensible.
UITableViewCells with Dynamic Type and
Let’s refresh our memory now and configure the initial table view controller. First make the
ViewController class be a subclass of
UITableViewController. Second, in Main.storyboard, delete the current view controller, drag a table view controller out, make its class be
ViewController, make it the “Is Initial View Controller” and embed it into a Navigation Controller. Finally, select the prototype cell, give it an identifier of “Cell” and a Basic style.
That’s it for the storyboard!
Open ViewController.swift and add a property called
projects of type array of array of strings! Inside
viewDidLoad, add a series of calls to
projects.apped() containing each an array of two strings, the title of the project and its subtitle.
Now implement the two main table view data source methods,
numberOfRowsInSection which will return
cellForRowAt which will dequeue a reusable cell with an identifier of “Cell” for the
indexPath, will declare a
project constant equal to the
indexPath.row index of the
projects array and set the text of the cell’s text label to show both the first and second element of the
project array before returning the cell.
Go now back to Main.storyboard for a small but important change: select the title label of the table view cell and makes its lines property in the Attributes Inspector be equal to 0.
Back in our view controller class, create a method to handle the appearance of the text called
makeAttributedString. This method will accept two parameters, a title and a subtitle, both of type
String and return an
NSAttributedString. Inside it, declare a
titleAttributes constant, of type Dictionary of
NSObject with two elements inside: a setting for the font to be the
UIFont.preferredFont(forTextStyle: .headline) and one for the foreground color to be purple. Then, declare a second constant for the subtitle’s attributes which will just contain the preferred font equal to
.subheadline. Then declare a
titleString of type
NSMutableAttributedString with a string composed of the
title parameter and a newline character and our title attributes as attributes. Consequently, create a subtitle string with the subtitle as string and the subtitle attributes as attributes. At this point, append the subtitle string to the title string and return this last one.
How to use
SFSafariViewController to browse a web page
SafariServices at the top of the
ViewController class. Create a method down in the class called
showTutorial() which accepts an integer parameter. Inside check that there exists an URL with the desired string using the integer parameter + 1 and, if that succeeds, declare a constant to hold the safari view controller’s configuration and set that configuration’s
entersReaderIfAvailable property to true. At this point just declare a new view controller of type
SFSafariViewController with our url and configuration constant as arguments and present it with an animation.
A little break to check what we can learn here. Here is the Documentation page for the
SFSafariViewController which is a subclass of
An object that provides a visible standard interface for browsing the web.
The view controller includes Safari features such as Reader, AutoFill, Fraudulent Website Detection, and content blocking. In iOS 9 and 10, it shares cookies and other website data with Safari. The user’s activity and interaction with
SFSafariViewControllerare not visible to your app, which cannot access AutoFill data, browsing history, or website data. You do not need to secure data between your app and Safari. If you would like to share data between your app and Safari in iOS 11 and later, so it is easier for a user to log in only one time, use
In accordance with App Store Review Guidelines, this view controller must be used to visibly present information to users; the controller may not be hidden or obscured by other views or layers. Additionally, an app may not use
SFSafariViewControllerto track users without their knowledge and consent.
UI features include the following:
- A read-only address field with a security indicator and a Reader button
- An Action button that invokes an activity view controller offering custom services from your app, and activities, such as messaging, from the system and other extensions
- A Done button, back and forward navigation buttons, and a button to open the page directly in Safari
- On devices that support 3D Touch, automatic Peek and Pop for links and detected data
Configuration class is just a configuration object that defines how a Safari view controller should be initialised and it has only two properties,
barCollapsingEnabled. Kind of little to make a class out of, ain’t it?
To finish this part of the tutorial we just need to implement the
didSelectRowAt table view delegate method and pass it the
showTutorial(indexPath.row) call inside.
How to add Core Spotlight to index your app content
Add a property to the class, of type array of integers, to store the user’s favorite projects. Inside
viewDidLoad() store the user defaults standard database and, if an object for the key “favorites” can be found and downcast as an array of integers, set the
favorites property to be equal to the found object.
viewDidLoad(), after this, just add the possibility for the table view to be in editing mode and to allow selection during editing.
cellForRowAt, add a check to see if the
favorites array contains the
indexPath.row integer and, if so, set the cell’s editing accessory type to
.checkmark, otherwise, set it to
editingStyleForRowAt delegate method and, inside, check that the
favorites array contains the
indexPath.row integer, in which care return
.delete otherwise return
commit editingStyle delegate method of table views so that, if the editing style is
.insert, we will append the
indexPath.row integer and call the yet unwritten
index(item:) method passing that same integer as the only argument. Otherwise we will bind the first index of that same integer to a constant if it exists and remove it from the
favorites array and also call the yet unwritten
deindex(item:) method passing that integer as the only argument. Then access the user defaults standard database and set the
favorites array for the key “favorites” just before calling the
.reloadRows method on the table view with an array containing the
.none as arguments.
Here is a quick description of the method we just implemented:
Asks the data source to commit the insertion or deletion of a specified row in the receiver.
When users tap the insertion (green plus) control or Delete button associated with a
UITableViewCellobject in the table view, the table view sends this message to the data source, asking it to commit the change. (If the user taps the deletion (red minus) control, the table view then displays the Delete button to get confirmation.) The data source commits the insertion or deletion by invoking the
deleteRows(at:with:), as appropriate.
To enable the swipe-to-delete feature of table views (wherein a user swipes horizontally across a row to display a Delete button), you must implement this method.
You should not call
setEditing(_:animated:)within an implementation of this method. If for some reason you must, invoke it after a delay by using the
tableView: The table-view object requesting the insertion or deletion.
editingStyle: The cell editing style corresponding to a insertion or deletion requested for the row specified by indexPath. Possible editing styles are
indexPath: An index path locating the row in tableView.
MobileCoreServices. Let’s look briefly at these two frameworks:
Index your app so users can search the content from Spotlight and Safari.
You can help users access activities and items within your app by making your content searchable. The Core Spotlight framework provides APIs to label and manage persistent user data like photos, contacts, and purchased items in the on-device index, and allows you to create links into your app.
The Core Spotlight APIs do not make items publicly searchable. Instead, Core Spotlight enables you to make items searchable in the user’s private, on-device index, the contents of which is never shared with Apple or synced between devices.
iOS provides additional strategies for making your app’s content searchable:
Core Spotlight enables you to index content at any point, such as when the app loads, and the Core Spotlight APIs do not require users to visit the content in order to index it. Core Spotlight works best when you have no more than a few thousand items.
A few thousand items … Good Lord!
Use uniform type identifier (UTI) information to create and manipulate data that can be exchanged between your app and other apps and services.
This collection of documents describes the programming interfaces for working with the system-declared uniform types.
We actually want to look at just this document:
Uniform Type Identifiers (or UTIs) are strings which uniquely identify abstract types. They can be used to describe a file format or an in-memory data type, but can also be used to describe the type of other sorts of entities, such as directories, volumes, or packages.
Type declarations appear in bundle property lists and tell the system several things about a type. […]. A few key concepts that are found in the declaration include:
Type declarations may include several other properties: a localizable user description of the type, the name of an icon resource in the declaring bundle, a reference URL identifying technical documentation about the type itself, and a version number, which can be incremented as a type evolves. All of these properties are optional.
All this is very cryptic but I thought a read was worthy, at least to realise how behind in progress one actually is!
Now let’s complete the
index(item:) method. First, declare a constant that extracts the item of the
projects array at the
item index. Second, declare a constant called
attributeSet of type
CSSearchableItemAttributeSet with an initialiser using
kUTTypeText as String (which tells iOS we want to store text in our indexed record).
This type represents the set of properties to display for a searchable item.
To make content searchable, create an attribute set that contains properties that specify the metadata to display about an item (represented by a
CSSearchableItemobject) when it appears in a search result.
The attributes you choose depend on your domain. You can use the properties that Core Spotlight provides in categories defined on
CSSearchableItemAttributeSet(such as Media and Documents), or you can define your own. If you want to define a custom attribute, be as specific as possible in your definition and use the
contentTypeTreeproperty so that your custom attribute can inherit from a known type.
CSSearchableItemAttributeSetobject should be changed from only one thread at a time. Concurrent access to properties in an attribute set has undefined behavior.
Once this is declared set its
title property to
project and its
contentDescription property to
Third, declare an
item constant of type
CSSearchableItem, with an unique identifier of
"\(item)", a domain identifier that can be actually anything by now as long as it is a string and the just created
attributeSet as the attribute set.
Fourth, instantiate the Core Spotlight searchable index default and call the
indexSearchableItems method on it. This is a closure so it will be executed asynchronously in order to tell us whether the indexing was successful or not.
Adds or updates items in the index.
searchableIndex(_:reindexSearchableItemsWithIdentifiers:acknowledgementHandler:)protocol method is called in the case that the journaling completed successfully but the data was not able to be indexed for some reason.
The block receives the following parameter:
So here I am, once more at Turin’s airport at night, spending my wait until the plane coding! Happy Day 137 to me!
deindex(item:) method is very similar to the previous one but shorter as it just calls the
.deleteSearchableItems() method on thee default searchable index of Core Spotlight. This method has, as last parameter, a closure which will handle error catching or success communication to the system.
Now switch to AppDelegate.swift and add the
import CoreSpotlight line at the top. Implement the
application(_:continue:restorationHandler:) method: it will check that the activity type of the user activity will be equal to
CSSearchableItemActionType and, if so, it will conditionally bind the value of the user activity’s
userInfo dictionary corresponding to the
CSSearchableItemActivityIdentifier key to the
uniqueIdentifier constant and will try to conditionally typecast it to a
String object. If this succeeds it will conditionally bind the window’s root view controller to the
navigationController constant and try to conditionally typecast it as a
UINavigationController. If also this succeeds, it will again conditionally bind the navigation controller’s top view controller to the
viewController and try to conditionally typecast it as a
ViewController. If all this works, we will call the
showTutorial() method on the view controller, passing it the typecast Integer version of the
uniqueIdentifier constant as its only argument. At the end of the method we will return
Now we can go and enjoy our finished app!
The first extension we are advised to perform in this app is to extend its longevity by making the
projects array be a collection of objects of a custom subclass. I performed this by creating a separate Swift file called Project.swift and, inside, declared a
struct Project with three properties, a
subtitle of type String and a
isFavorite of type Bool, which defaults too false.
I deleted the
favorites array, modified the appending of objects to the new
projects array inside
viewDidLoad and wrote two helper methods called
saveProjects to handle JSON encoding and decoding our custom type.
cellForRowAt I changed the arguments of the
makeAttributedString method to be
project.subtitle and, in the following line, replaced the condition of the
if statement with
editingStyleForRowAt I changed the condition of the
if statement with
commit:forRowAt: I changed both first lines of code in each of the
if-else branches to, respectively,
projects[indexPath.row].isFavorite = true and the same but
= false, removing any reference to the
favorites array. Here I also called
index(item:) method I changed the 3rd and 4th line to call
project.subtitle respectively and … that’s it!
The second extension was to test different
NSAttributedString features, which, given the time, I simply browsed in the Documentation.
For the third and final one we are not really being told what to do and given my previous experience with system notifications (in the scary Project 19), I’m not sure where to start with it. I am quite afraid of destroying everything. I will probably come back to this, but not now!
Thank you for reading until here!
Please don’t forget to drop a hello and a thank you to Paul for all his great work (you can find him on Twitter) and be sure to visit the 100 Days Of Swift initiative page. We are learning so much thanks to him and he deserves to know of our gratitude.
He has written about 20 great books on Swift, all of which you can check about here.
The 100 Days of Swift initiative is based on the Hacking with Swift book, which you should definitely check out.
If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a small donation to support my studies and my writing (please appreciate that I am not using advertisement on my articles).
If you are interested in my music engraving and my publications don’t forget visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).
You can also support me by buying Paul Hudson’s books from this Affiliate Link.
Anyways, thank you so much for reading!
Till the next one!