With Swift 2.0, Apple introduced the #available syntax for declaring code only usable in specific iOS versions. This is much improved from previous solutions but the default approach makes it hard to test. This post will show a technique to write testable iOS 9 only code.

Let’s start with an app that creates a Spotlight Search. A nice and easy implementation would look like this:

import Foundation
import CoreSpotlight

class SpotlightSearch {
    
    func doSearch() {
        if #available(iOS 9.0, *) {
            
            let attributeSet = CSSearchableItemAttributeSet(itemContentType: "MyContentType")
            attributeSet.title = "Some Title"
            attributeSet.contentDescription = "Some Description"
            attributeSet.keywords = ["Sharp", "Five"]
            
            let item = CSSearchableItem(uniqueIdentifier: "my-unique-search-item", domainIdentifier: "com.sharpfivesoftware.spotlightdomain", attributeSet: attributeSet)
            CSSearchableIndex.defaultSearchableIndex().indexSearchableItems([item], completionHandler: nil)
        } else {
            // Fallback on earlier versions
        }
    }
}

 

This class can be used like this:

override func viewDidLoad() {
        super.viewDidLoad()
        
        let spotlightSearch = SpotlightSearch()
        spotlightSearch.doSearch()
    }

This isn’t the best approach for the following reasons:

  1. doSearch silently does nothing on iOS8, the behavior is correct but not clear from reading the code.
  2. SpotlightSearch is not testable.
  3. ViewController is not testable

We can improve the ViewController testability by exposing a property for Spotlight Search.

class ViewController: UIViewController {

    var spotlightSearch = SpotlightSearch()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        spotlightSearch.doSearch()
    }
}

This allows us to write a test

import XCTest
@testable import AvailableSandbox

class ViewControllerTests: XCTestCase {

    func testDoSearchIsCalled() {
        let spotlightSearchMock = SpotlightSearchMock()
        
        let sut = ViewController()
        
        sut.spotlightSearch = spotlightSearchMock
        
        let _ = sut.view
        
        XCTAssertTrue(spotlightSearchMock.doSearchObserved)
    }
    
    
    class SpotlightSearchMock : SpotlightSearch {
        var doSearchObserved = false
        
        override func doSearch() {
            doSearchObserved = true
        }
    }

}

This improves testability and allows us to inject a property but the code still silently does nothing for iOS8. We also can’t test the Spotlight Search class.

Let’s try the same approach for the Spotlight Search:


class SpotlightSearch {
    
    var searchableIndex = CSSearchableIndex.defaultSearchableIndex()
    
    func doSearch() {
        if #available(iOS 9.0, *) {
            
            let attributeSet = CSSearchableItemAttributeSet(itemContentType: "MyContentType")
            attributeSet.title = "Some Title"
            attributeSet.contentDescription = "Some Description"
            attributeSet.keywords = ["Sharp", "Five"]
            
            let item = CSSearchableItem(uniqueIdentifier: "my-unique-search-item", domainIdentifier: "com.sharpfivesoftware.spotlightdomain", attributeSet: attributeSet)
            searchableIndex.indexSearchableItems([item], completionHandler: nil)
        }
    }
}

Unfortunately, this doesn’t compile. Swift wont allow us to have an partially available property. A solution is to mark the whole class as available for iOS9.

@available(iOS 9.0, *)
class SpotlightSearch {...

More trouble. This causes our ViewController code to not compile. Since the entire SpotlightSearch class is now iOS9 only, the ViewController can’t use it. We have a chicken and egg problem. How can we test both classes?

The solution? Create a Optional protocol for the interface of SpotlightSearch. Consuming classes like ViewController will build it conditionally based on the iOS version.

protocol SpotlightSearchProtocol {
    func doSearch()
}

@available(iOS 9.0, *)
class SpotlightSearch : SpotlightSearchProtocol {
    
    var searchableIndex = CSSearchableIndex.defaultSearchableIndex()
    
    func doSearch() {        
        let attributeSet = CSSearchableItemAttributeSet(itemContentType: "MyContentType")
        attributeSet.title = "Some Title"
        attributeSet.contentDescription = "Some Description"
        attributeSet.keywords = ["Sharp", "Five"]
        
        let item = CSSearchableItem(uniqueIdentifier: "my-unique-search-item", domainIdentifier: "com.sharpfivesoftware.spotlightdomain", attributeSet: attributeSet)
        searchableIndex.indexSearchableItems([item], completionHandler: nil)
    }
}
class ViewController: UIViewController {

    var spotlightSearch: SpotlightSearchProtocol? = {
        if #available(iOS 9, *) {
            return SpotlightSearch()
        }
        return nil
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        if let spotlightSearch = spotlightSearch {
            spotlightSearch.doSearch()
        }
    }
}

We have testability for partially available methods and classes.
The only downside is the additional overhead needed to create and manage the protocol.

 

To recap:
  • Mark your class/struct iOS 9 only.
  • Extract a protocol from the interface.
  • Use the class as an Optional property on the calling class.
  • Conditionally build (or inject) the property based on the current OS.

Join the conversation!