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).