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.
Setting up
Create a new Single View App Xcode Project, call it “Swift Searcher” and save it somewhere sensible.
Automatically resizing UITableViewCell
s with Dynamic Type and NSAttributedString
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 projects.count
and 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 NSAttributedString.Key
and 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
Import 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 UIViewController
:
Summary
An object that provides a visible standard interface for browsing the web.
Declaration
Discussion
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
SFSafariViewController
are 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, useSFAuthenticationSession
instead.Important
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
SFSafariViewController
to 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
The Configuration
class is just a configuration object that defines how a Safari view controller should be initialised and it has only two properties, entersReaderIfAvailable
and 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.
That’s it…!
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.
Always in viewDidLoad()
, after this, just add the possibility for the table view to be in editing mode and to allow selection during editing.
Now, inside 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 .none
.
Implement the editingStyleForRowAt
delegate method and, inside, check that the favorites
array contains the indexPath.row
integer, in which care return .delete
otherwise return .return
.
Override the 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 indexPath
and .none
as arguments.
Here is a quick description of the method we just implemented:
Summary
Asks the data source to commit the insertion or deletion of a specified row in the receiver.
Declaration
Discussion
When users tap the insertion (green plus) control or Delete button associated with a
UITableViewCell
object 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 theUITableView
methodsinsertRows(at:with:)
ordeleteRows(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 theperform(_:with:afterDelay:)
method.Parameters
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 areUITableViewCell.EditingStyle.insert
orUITableViewCell.EditingStyle.delete
.
indexPath
: An index path locating the row in tableView.
Import CoreSpotlight
and MobileCoreServices
. Let’s look briefly at these two frameworks:
Framework
Core Spotlight
Index your app so users can search the content from Spotlight and Safari.
Overview
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!
Framework
MobileCoreServices
Use uniform type identifier (UTI) information to create and manipulate data that can be exchanged between your app and other apps and services.
Overview
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:
UTType
Overview
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.
Declaration
Discussion
To make content searchable, create an attribute set that contains properties that specify the metadata to display about an item (represented by a
CSSearchableItem
object) 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 thecontentTypeTree
property so that your custom attribute can inherit from a known type.A
CSSearchableItemAttributeSet
object 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[0]
and its contentDescription
property to project[1]
.
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.
Summary
Adds or updates items in the index.
Declaration
Discussion
The
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.Parameters
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!
The 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 true
.
Now we can go and enjoy our finished app!
Challenges
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 title
and 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 loadProjects
and saveProjects
to handle JSON encoding and decoding our custom type.
Inside cellForRowAt
I changed the arguments of the makeAttributedString
method to be project.title
and project.subtitle
and, in the following line, replaced the condition of the if
statement with project.isFavorite
.
Inside editingStyleForRowAt
I changed the condition of the if
statement with projects[indexPath.row].isFavorite
. In 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 saveProjects()
.
In the index(item:)
method I changed the 3rd and 4th line to call project.title
and 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!