Learning Swift — Days 136 to 137

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 UITableViewCells 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, use SFAuthenticationSession 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 the UITableView methods insertRows(at:with:) or 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 perform(_: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 are UITableViewCell.EditingStyle.insert or UITableViewCell.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 the contentTypeTree 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!

Published by Michele Galvagno

Professional Musical Scores Designer and Engraver Graduated Classical Musician (cello) and Teacher Tech Enthusiast and Apprentice iOS / macOS Developer Grafico di Partiture Musicali Professionista Musicista classico diplomato (violoncello) ed insegnante Appassionato di tecnologia ed apprendista Sviluppatore iOS / macOS

Leave a comment