Archive

Archive for the ‘Programming’ Category

Testing for `isEmpty` with Swift Optionals

Testing for an empty string is a pretty common use case. Swift makes it easy with the isEmpty computed property, which the API docs tell us returns “true iff self contains no characters”. That is, if self == “”.

var emptyStringLiteral = ""
emptyStringLiteral.isEmpty // true

var nonEmptyStringLiteral = "foo"
nonEmptyStringLiteral.isEmpty // false

But it’s also pretty common to expand the definition of “empty” to include nil. In Swift, a value that may be nil is modeled as an Optional, but unfortunately, the Optional type is an enum wrapping its value, so it doesn’t include an isEmpty property. So, testing Optional strings means you have to unwrap. You could use let binding:

var optionalString: String?
// Do some work
if let s = optionalString where s != "" {
    // s is not empty or nil
}

Or slightly more succinctly, use the nil coalescing operator “??”:

if !(optionalString ?? "").isEmpty {
    // optionalString is not empty or nil
}

But really, we’d like to just use isEmpty directly on our Optional String—it just feels more Swifty:

if !optionalString.isEmpty {
    // optionalString is not empty or nil
}

Luckily, a small protocol extension will let us accomplish this:

protocol IsEmptyTestable {
    var isEmpty: Bool { get }
}

extension Optional where Wrapped: IsEmptyTestable {
    /// `true` if the Wrapped value is `nil`,
    /// otherwise returns `wrapped.isEmpty`
    var isEmpty: Bool {
        switch self {
        case .Some(let val):
            return val.isEmpty
        case .None:
            return true
        }
    }
}

extension String: IsEmptyTestable {}

And with that in place, we can now test both Optional and non-Optional Strings with the isEmpty property!

var nilOptionalString: String? = nil
nilOptionalString.isEmpty // true

var emptyOptionalString: String? = ""
emptyOptionalString.isEmpty // true

var nonEmptyOptionalString: String? = "foo"
nonEmptyOptionalString.isEmpty // false

var emptyStringLiteral = ""
emptyStringLiteral.isEmpty // true

var nonEmptyStringLiteral = "foo"
nonEmptyStringLiteral.isEmpty // false

So let’s look at what’s going on with that extension.

What we’d really like to do is define an extension on Optional. However, Swift won’t allow that (at least not yet). Trying to declare the extension directly on the generic results in this compiler error:

extension Optional<String> { }
// error: constrained extension must be declared on the unspecialized generic
// type 'Optional' with constraints specified by a 'where' clause

Fair enough, let’s put our String requirement in a where clause. We can look at the public API in Xcode to see that Optional’s associated type is called Wrapped, so let’s see if we can require Wrapped to be a String:

extension Optional where Wrapped == String { }
// error: same-type requirement makes generic parameter 'Wrapped' non-generic

Oops. Why should this be happening? The good news is, it’s considered a bug, and will hopefully be fixed in a future version of Swift.

In the meantime, though, we can work around this with an intermediate protocol to declare our behavior, and then instructing the compiler that String conforms to that protocol.

So, we define a protocol that requires an isEmpty property that returns a Bool:

protocol IsEmptyTestable {
    var isEmpty: Bool { get }
}

Then we tell the compiler that the Swift type conforms to that protocol (and since we defined the protocol to match the signature provided by the standard library, we know it will conform):

extension String: IsEmptyTestable {}

Then we write the default implementation for Optional.isEmpty:

extension Optional where Wrapped: IsEmptyTestable {
    /// `true` if the Wrapped value is `nil`, otherwise returns `wrapped.isEmpty`
    var isEmpty: Bool {
        switch self {
        case .Some(let val):
            return val.isEmpty
        case .None:
            return true
        }
    }
}

Note the switch for unwrapping the value: remember that Swift optionals are just enums! That lets us use a very efficient switch statement to access the optional’s associated value and call its isEmpty property.

And then we’re on a roll. We can extend this to other Swift standard library types that already have an isEmpty property simply by declaring that the Wrapped type conforms to IsEmptyTestable:

extension Array: IsEmptyTestable {}
var nilOptionalArray: [Int]?
nilOptionalArray.isEmpty // true

extension Set: IsEmptyTestable {}
var nilOptionalSet: Set<Int>?
nilOptionalSet.isEmpty // true

extension Dictionary: IsEmptyTestable {}
var nilOptionalDictionary: [String: String]?
nilOptionalDictionary.isEmpty // true

And so on.

One final caveat: Xcode really wants to help you, so you might find it helpfully inserting “?” to unwrap optional values for you:

if nilOptionalString?.isEmpty { } // Xcode, stahp!
// error: optional type '_?' cannot be used as a boolean; test for '!= nil'
// instead

So just remember to tell Swift that no, in this case you really want to be operating on the Optional itself:

if nilOptionalString.isEmpty { }

And there you have it: an isEmpty implementation for Optionals.

Happy Swifting!

Categories: Programming
%d bloggers like this: