The personal blog of Stuart Breckenridge

Some Answers to the Question on Nested Enums

A few days ago I asked if it was possible to write a function using the outermost enum to reference the nested enums. The example code:

enum ArmyRanks {
    enum Commissioned {
        case SecondLieutenant, Lieutenant, Captain
    }
    
    enum NonCommissioned {
        case Private, LanceCorporal, Corporal
    }
}

// Desired outcome:

struct Soldier {
    let rank:ArmyRanks
}

let s = Soldier(rank: .Commissioned.Captain) 

Solution 1

Separate the outermost enum, make it a protocol, and make the nested enums conform to the protocol:

protocol ArmyRanks {}

enum Commissioned:ArmyRanks {
        case SecondLieutenant, Lieutenant, Captain
    }
    
enum NonCommissioned:ArmyRanks {
        case Private, LanceCorporal, Corporal
    }
    
struct Soldier {
    let rank: ArmyRanks
}

let s = Soldier(rank: Commissioned.Captain)

switch soldier.rank
{
    case is Commissioned:
        print("Is a commissioned officer: \(soldier.rank)")
    case is NonCommissioned:
        print("Is a non-commissioned officer: \(soldier.rank)")
    default:
        break
}

Solution 2

Add cases with associated values to the outermost enum:

enum Ranks{
    case Commissioned(CommissionedRanks)
    case NonCommissioned(NonCommissionedRanks)
    
    enum CommissionedRanks {
        case SecondLieutentant, Lieutentant, Captain
    }
    
    enum NonCommissionedRanks {
        case Private, LanceCorporal, Corporal
    }
}

struct Soldier {
    let rank:Ranks
}

let soldier = Soldier(rank: .Commissioned(.Captain))

switch soldier.rank
{
case .Commissioned(let description):
		print("Commissioned Officer: \(description)") // prints "Commissioned Officer: Captain"
case .NonCommissioned(let description):
		print("Non Commissioned Officer: \(description)")
}

The latter of these two solutions is my preferred approach.

Credit to @ryanbooker on the Swift-Lang Slack channel for proposing both solutions.


A Question on Nested Enums

I have a feeling the answer to this little experiment is no, but I thought I’d ask about it anyway.

Take the following example of a nested enumeration of commissioned and non-commissioned Army ranks:

enum ArmyRanks {
    enum Commissioned {
        case SecondLieutenant, Lieutenant, Captain
    }
    
    enum NonCommissioned {
        case Private, LanceCorporal, Corporal
    }
}

If I want to create a struct of a soldier using the above enum to denote rank, I could do this:

struct Soldier {
    let rank:ArmyRanks.Commissioned
}


Or I could do this:

struct Soldier {
    let rank:ArmyRanks.NonCommissioned
}


I’d like to be able to write the struct referencing only ArmyRanks and then be able to provide .Commissioned, or .NonCommissioned as needed.

struct Soldier {
    let rank:ArmyRanks
}


Is this possible?


Searchable App Content with Core Spotlight

Nothing to see in search...yet!

The Core Spotlight API allows developers to add their app’s content to the on-device index on iOS devices, allowing that content to be searched and accessed directly using the search bar on the home screen. In this post, we’ll add functionality that will both add and delete content to the on-device index, in addition to restoring application state when the user accesses the app via a Core Spotlight search result.

First, let’s discuss what is stored in the on-device index.

Elements in the on-device index are CSSearchableItems. A CSSearchableItem is initialised with a uniqueIdentifier, an optional domainIdentifier, a CSSearchableItemAttributeSet, and, optionally, an expiryDate. The CSSearchableItemAttributeSet contains the properties that are displayed to the user in the search results, for example, the title and the thumbnailData.

In the example code that follows:

  • We will, at the request of the user, add each version of OS X in the OSX.history array to the on-device index.
  • Each version of OS X will correspond to a single CSSearchableItem.
  • The CSSearchableItemAttributeSet of each CSSearchableItem will contain the name of the OS, the OS icon, and the OS description.
  • We will provide a method to remove the CSSearchableItems from the on-device using their domainIdentifier.
  • When the user access the app via a Core Spotlight search, we will highlight the OS version that was selected when the app is restored to the foreground.

Adding to the On-Device Index

To keep things simple, a new UIBarButtonItem has been added to the navigation bar which will call the following method:

@IBAction func presentSpotlightOptions(sender: AnyObject)

To spare you the boiler plate, this method will present a UIAlertController to the user allowing them either Add to Core Spotlight, Remove from Core Spotlight, or dismiss the controller.

To create CSSearchableItems, their respective CSSearchableItemAttributeSets, and add them to the index, we will extend the functionality of the OSX class with a new method:

func addHistoryToCoreSpotlight(result:CoreSpotlightResult)

In this method, we initially create a temporary [CSSearchableItem] array, and then enumerate over the OSX.history array. For each dictionary entry in the OSX.history array, a CSSearchableItemAttributeSet is created and provided with the name of the OS, the description of the OS, and the icon image of the OS, for its respective title, contentDescription, an array of keywords, and thumbnailData properties.

Following the creation of the CSSearchableItemAttributeSet, we initialise a CSSearchableItem and provide it with the entry’s index as the uniqueIdentifier, a static domainIdentifier of com.osxhistory.indexedItems, and the aforementioned CSSearchableItemAttributeSet. Each CSSearchableItem is added to the temporary [CSSearchableItem] array.

The code for this is shown below:

var itemsToAdd = [CSSearchableItem]() 
        
for (index, os) in history.enumerate()
{
	let uniqueID = index 

	let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeText as String) 
	attributeSet.title = os["name"] as? String! 
	attributeSet.contentDescription = os["description"] as? String! 
	attributeSet.thumbnailData = UIImagePNGRepresentation(UIImage(named: (os["image"] as? String!)!)!) 
	attributeSet.keywords = ["OS X", (os["name"] as? String!)!, (os["version"] as? String!)!] 

	let searchableItem = CSSearchableItem(uniqueIdentifier: String(uniqueID), domainIdentifier: "com.osxhistory.indexedItems", attributeSet: attributeSet) 
	searchableItem.expirationDate = NSDate().dateByAddingTimeInterval(600) 

	itemsToAdd.append(searchableItem) 
}

In the example above the CSSearchableItem has been set with an expirationDate of 10 minutes from the point it was created. This means that 10 minutes after the data is indexed, it will be automatically removed.

Once this is complete, we are ready to have the content indexed and to do that, we call the following method:

CSSearchableIndex.defaultSearchableIndex().indexSearchableItems(itemsToAdd) { (error) in
	if error != nil
	{
		NSOperationQueue.mainQueue().addOperationWithBlock({
			completionHandler(error: error!)
		})
	} else{
		NSOperationQueue.mainQueue().addOperationWithBlock({
			completionHandler(error: nil)
		})
	}
}

In the implementation above the CoreSpotlightResult1 completionHandler is called on the main thread after the CSSearchableItems have been journaled by the index. We respond to the CoreSpotlightResult by displaying a success or error message depending whether an NSError is provided by the block.

With all this work complete we can now search for our app’s content using the Spotlight search bar.

Result!

Restoring State

Without any additional code, tapping on an OS X History result will open the app and do nothing. That’s not very interesting! Let’s do something quite contrived.

In the app delegate, add the following code:

func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool {
        
        if userActivity.activityType == CSSearchableItemActionType {
            let uniqueId = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String
            let navigationController = window?.rootViewController as? UINavigationController
            let viewController = navigationController?.topViewController as? ViewController
            viewController?.restoreState(Int(uniqueId!)!)
        }
        
        return true
    }

This method lets the app delegate know that data is available to restore state or continue an activity. What we’re doing above is extracting the uniqueId of the CSSearchableItem (which was its index in the OSX.history array) and then passing it to a new method—restoreState(row:Int)—on the ViewController.

The restoreState(row:Int) method will scroll to indexPath of the selected search result and then, magically, spin the OS X icon.

// Create an `indexPath` and scroll to the `indexPath.row`.
let path = NSIndexPath(forRow: row, inSection: 0)
osXTableView.scrollToRowAtIndexPath(path, atScrollPosition: .Top, animated: false)

// Rotate the image view of the cell at the indexPath
let cell = osXTableView.cellForRowAtIndexPath(path) as! OSXCell
let rotation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.toValue = M_PI * 2.0
rotation.duration = 1.0
rotation.repeatCount = 1
rotation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
cell.imageView?.layer.addAnimation(rotation, forKey: "rotationAnimation")

Removing from the On-Device Index

To remove data from the index, we can tap our UIBarButtonItem again, and select Remove from Core Spotlight. This will then call the following method on the OSX class:

func removeHistoryFromCoreSpotlight(completionHandler: CoreSpotlightResult)
    {
        CSSearchableIndex.defaultSearchableIndex().deleteSearchableItemsWithDomainIdentifiers(["com.osxhistory.indexedItems"]) { (error) in
            if error != nil
            {
                NSOperationQueue.mainQueue().addOperationWithBlock({
                    completionHandler(error: error!)
                })
            } else{
                NSOperationQueue.mainQueue().addOperationWithBlock({
                    completionHandler(error: nil)
                })
            }
        }
    }

This removes all CSSearchableItems with the domainIdentifier of com.osxhistory.indexedItems. In short, all our OS X entries will be removed from the index. Like the addHistoryToCoreSpotlight method, the CoreSpotlightResult block is called at the conclusion of the method and will pass an error if there has been a problem deleting the data.2

Wrapping Up

In this post, using the Core Spotlight API, we’ve achieved the following:

  • Adding data to the on-device index
  • Removing data from the on-device index
  • Restoring state by reading the NSUserActivity data passed to the app delegate

Updated app code is available on GitHub.

  1. typealias CoreSpotlightResult = (error:NSError?) -> () ↩︎

  2. If you wish to see errors, try running this code in the iPhone 4s simulator. ↩︎


Swift 3.0 Release Process

From the Swift Blog:

Swift 3.0 is a major release that is not source-compatible with Swift 2.2. It contains fundamental changes to the language and Swift Standard Library. A comprehensive list of implemented changes for Swift 3.0 can be found on the Swift evolution site.

Swift 3.0 is expected to be released sometime in late 2016. In addition to its Swift.org release, Swift 3.0 will ship in a future version of Xcode.

Exciting times.


Default .gitignore for iOS and OS X Projects

Whenever you start a new Xcode project you are presented with the option of creating a Git repository on your Mac or on a server. The issue with this is that you don’t get a .gitignore file by default, so your initial commit is full of stuff you don’t necessarily need, like .DS_Store. There is an option to provide a default .gitignore when a new repository is initialised.

First, open/create your .gitconfig file in Terminal:

nano ~/.gitconfig

Add the following configuration details:

[core]
        excludesfile = ~/.gitignore

Save the file: Ctrl + X, Y, Enter

Then, open/create a new .gitignore at root level:

nano ~/.gitignore

Add the following configuration details to ensure that git intentionally will not track these files:

# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## Build generated
build/
DerivedData

## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata

## Other
*.xccheckout
*.moved-aside
*.xcuserstate
*.xcscmblueprint

## Obj-C/Swift specific
*.hmap
*.ipa

## Playgrounds
timeline.xctimeline
playground.xcworkspace

# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
.build/

# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/

# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts

Carthage/Build

# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md

fastlane/report.xml
fastlane/screenshots

.DS_Store

Save the file: Ctrl + X, Y, Enter

Subsequent git repositories will read from this .gitignore file when being initialised.

Note: If you wish to override the patterns in this .gitignore, you can add a local (to the repo) .gitignore that negates the pattern. For example, if you wanted to include playground.xcworkspace in the commit, you would add !playground.xcworkspace to the local .gitignore file.

Recommended Reading:

  1. Git - gitignore Documentation (via Git-SCM).


Implementing UISearchController

With iOS 8, Apple introduced a more streamlined approach to incorporating a UISearchBar and associated search functionality. In this post, we look at how to implement UISearchController.

Before we look at UISearchController, we need to create a UITableView and seed it with some data. I’ll spare you the boilerplate of creating a table view, but with WWDC around the corner—where we’re going to learn about the future of OS X (or is it macOS?)—my data source is a run down of the history of OS X releases. Topical, right? The data source—history—is created in a separate class called OSX, along with a searchResults array:

class OSX {
    static let sharedOSX = OSX()
    
    var history:[Dictionary<String,Any>] = [
        [
            "name" : "Cheetah",
            "version" : "10.0",
            "released" : "March 24, 2001",
            "image" : "Cheetah"
        ],
        [
            "name" : "Puma",
            "version" : "10.1",
            "released" : "September 25, 2001",
            "image" : "Cheetah"
        ],
        [
            "name" : "Jaguar",
            "version" : "10.2",
            "released" : "August 23, 2002",
            "image" : "Jaguar"
        ]
        
        // and so on...
        
        
        ]
        
    var searchResults = [Dictionary<String,Any>]()
        
}

When populated into the tableview it looks like this:

What do we do to add a UISearchController to the tableview? It’s really quite simple. Within your tableview’s view controller, create a lazy variable for your UISearchController and configure it as you see fit. My example implementation is below:

lazy var searchController:UISearchController = ({
        let controller = UISearchController(searchResultsController: nil) // 1
        controller.hidesNavigationBarDuringPresentation = false // 2
        controller.dimsBackgroundDuringPresentation = false // 3
        controller.searchBar.searchBarStyle = .Minimal // 4
        controller.searchResultsUpdater = self // 5
        return controller
    })()

In the order of what is going on here:

  1. If you want to present search results in the current view controller, pass nil as the parameter.
  2. I want to keep the UINavigationBar visible when using the search bar, so this property is set to false.
  3. If you want the background to be dimmed during a search, use true, for this implementation, I’m keeping it as false.
  4. It’s personal preference which search bar style you use. I’m using .Minimal.
  5. In order for a UISearchController to work, you have to assign an object that conforms to the UISearchResultsUpdating protocol. For this sample code, I’m assigning it to self, which is the view controller.

To make the searchController’s searchBar visible, you add a one-liner in viewDidLoad() setting the searchBar as the tableHeaderView:

osXTableView.tableHeaderView = searchController.searchBar

We then need to ensure that our view controller conforms to the UISearchResultsUpdating protocol:

extension ViewController:UISearchResultsUpdating
{
    func updateSearchResultsForSearchController(searchController: UISearchController) {
        OSX.sharedOSX.searchResults = OSX.sharedOSX.history.filter({
            ($0["name"] as! String).lowercaseString.containsString(searchController.searchBar.text!.lowercaseString) ||
            ($0["version"] as! String).lowercaseString.containsString(searchController.searchBar.text!.lowercaseString) ||
            ($0["released"] as! String).lowercaseString.containsString(searchController.searchBar.text!.lowercaseString)
        })
        osXTableView.reloadData()
    }
}

In this method we are updating the searchResults array by filtering the history array based on the text in the searchController’s searchBar1. Once the array has been filtered, the tableView is reloaded.

Finally, in order to ensure that the searchResults are displayed correctly, we have to make two small amendments to numberOfRowsInSection and cellForRowAtIndexPath to accomodate for the searchController being active:

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        switch searchController.active {
        case true:
            return OSX.sharedOSX.searchResults.count
        case false:
            return OSX.sharedOSX.history.count
        }
    }
    
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
	let cell = tableView.dequeueReusableCellWithIdentifier("OSXCell") as! OSXCell
	
	switch searchController.active {
	case true:
		cell.configureSearchCell(indexPath)
	case false:
		cell.configureCell(indexPath)
	}
	
	return cell
}

The only difference between configureSearchCell and configureCell is the array from which each method retrieves data.

Once all the code is in place, we now have a working search bar!

The source code for this example is available on Github.

Recommended Reading:

  1. UISearchController Class Reference (via Apple)
  2. UISearchResultsUpdating Protocol Reference (via Apple)
  1. The strings contained within the data source and the search bar are lowercased so that, for example, “Ee” in the search bar will correctly match the “ee” from Cheetah. ↩︎


Performance Analysis of appendIfUnique

Last week we looked at extending Array to include an appendIfUnique function. In this post—inspired by feedback from Twitter—we assess the performance of that function against the built in contains(element: Self.Generator.Element) and Swift’s Set type.

The difference between O(n2) and O(n) can be seen in this graph:

via justin.abrah.ms

To see how close we are to O(n2) I’ve created three test cases:

  • one will insert n elements to a Set and then create an Array from that set;
  • one will append n unique elements to an Array using appendIfUnique; and,
  • one will append n unique elements to an Array using the contains(element:Self.Generator.Element) function
struct Benchmarks { 
    static func testNumbersSetToArray(size:Int) {
        var set = Set<Int>()
        
        while set.count < size {
            set.insert(set.count + 1)
        }
        
        let _ = Array(set)
    }
    
    static func testAppendIfUnique(size:Int) {
        var numbers = [Int]()
        
        while numbers.count < size {
            numbers.appendIfUnique(numbers.count + 1)
        }
    }
    
    static func testArrayContains(size:Int) {
        var numbers = [Int]()
        
        while numbers.count < size {
            if !numbers.contains(numbers.count + 1)
            {
                numbers.append(numbers.count + 1)
            }
        }
    }
}

Function performance is measured from XCTestCase by calling:

    func testAppend() {
        self.measureBlock {
            Benchmarks.testAppendIfUnique(60000)
        }
    }
    
    func testArrayContains(){
        self.measureBlock {
            Benchmarks.testArrayContains(60000)
        }
    }
    
    func testSet(){
        self.measureBlock {
            Benchmarks.testNumbersSetToArray(60000)
        }
    }

I ran tests with n going up to 60,000 and the compiler optimisation level set to Fast[-0] Whole Module Optimization. The results are below:

Elements appendIfUnique contains Set
10000 0.015s 0.029s 0.002s
20000 0.064s 0.112s 0.003s
30000 0.141s 0.251s 0.004s
40000 0.243s 0.450s 0.004s
50000 0.421s 0.695s 0.006s
60000 0.607s 1.019s 0.007s
Performance Results

Conclusion: Using Set is an outright winner, while appendIfUnique and contains are in line with O(n2). contains is, however, considerably slower than appendIfUnique which is surprising.

The source code for these tests is available on GitHub.

Recommended Reading:


Extending Array to Enforce Unique Elements

On the subject of our inability to pick random numbers, Keith Hillman writes1:

You might think that you know how to choose a random number but in all likelihood you are probably falling for a number of common mistakes that are giving the game away.

Software can solve this issue. I’m going to write a function that generates six random numbers for my next lottery ticket and will then store those numbers in an Array. The problem is that I can’t stop the random number function from generating duplicates and, out of the box, an Array will not stop duplicate values from being appended. Let’s have a look at how to enforce uniqueness in an Array.

First, let’s see what the basic code looks like before the problem is solved.

var numbers = [Int]() 

while numbers.count < 6 {
    numbers.append((Int(rand() % 49)) + 1) 
}

If you run this a few times you’ll begin to see duplicates appear in the numbers array.

[29, 1, 15, 12, 15, 17]

To stop that from happening you could generate your numbers, add them to a Set, check it has a count of six and then add them to an Array, or you could iterate over each number in the array and compare it to every other number in the array, or as we’re about to do, you could extend the array to include a new appendIfUnique function that can be reused easily.

extension Array where Element:Equatable 
{
    mutating func appendIfUnique(newElement:Element){
        var unique:Bool = true
        for item in self where newElement == item 
        {
            unique = false
            break
        }
        if unique
        {
            self.append(newElement)
        }
    }
}

What we are doing here is extending Array with new functionality in situations where the Elements stored in the array conform to the Equatable2 protocol. Types that conform to Equatable can be compared for value equality using == and !=.

The function is declared as mutating because we are modifying the array from within the function. A boolean—unique—is used to track the result from the equality test. Looping through the array with the for statement, the unique boolean will be changed to false when the element to be added has the same value as the the element being checked, at which point the for loop will break. Finally, if unique remains true at the end of the for loop, the newElement is added to the array.

In use, the revised code is almost identical:

var numbers = [Int]() 

while numbers.count < 6 {
    numbers.appendIfUnique((Int(rand() % 49)) + 1) 
}

You can try this code using the IBM Swift Sandbox.

  1. Why We Can’t Choose Random Numbers, http://www.psychology24.org ↩︎

  2. Equatable Protocol Reference, developer.apple.com ↩︎


The Cost of Attending WWDC

Inspired by Casey Liss’ writeup on how much it would cost to attend WWDC, I thought I’d price up a hypothetical trip from Singapore (prices in USD):

  • Flights: $1,484 (United Airlines—Economy) or $5,230 (United Airlines—Business)
  • Hotel: $2,906 (Parc 55, 6 nights)
  • WWDC ticket: $1,599

Total: $5,989 up to $9,735

Gulp. As I said in my previous post, I’ll be streaming the sessions.


Apple TV and the Importance of The

Siri on the new Apple TV is excellent. It saves time and lets you bypass a lot of navigation. However, I find it can be a bit picky about certain requests.

For example, I’m currently re-watching The West Wing. Instead of navigating through the TV series UI, I’ll say something like this:

"Show me West Wing series five."

Apple TV will respond with on screen text telling me it can’t find such a show. So I’ll repeat myself to Siri:

"Show me The West Wing series five."

…and this time Apple TV works fine. Because I included “the” in my request.

It’s a minor frustration that Siri is this sensitive to a missed word.