Skip to content

Instantly share code, notes, and snippets.

@jparishy
Last active August 29, 2015 14:10
Show Gist options
  • Select an option

  • Save jparishy/602bfeaca24fdc778ffe to your computer and use it in GitHub Desktop.

Select an option

Save jparishy/602bfeaca24fdc778ffe to your computer and use it in GitHub Desktop.

Variable types in Swift for Objective-C Developers

This introduction to generic types, protocols, and enums in Swift is part 2 of a series on Swift for Objective-C Developers. For part 1 about Optional Values, please click here. TODO LINK

This post assumes you are familiar with the idea of generics, but either haven't used them in practice or have been burnt by other languages' (looking at you, C++) implementations and are reluctant to embrace them in Swift.

In Objective-C we have the almightly id, the basic representation of all object types in the language. We often use it as a catch-all when we don't necessarily know the type of an object we're going to be working with. Most often, it is used as the type for objects managed by a collection. Because we do not want to have a different class for every object type we want to contain in an array we use an array type that accepts objects of type id, so we can use any type we like. This is certainly better than having multiple classes such as NSStringArray, NSNumberArray, or NSIndexPathArray, etc. for each type we want to place in a container.

But, we can do even better!

There are very obvious flaws to this approach. It is important to remember that the Foundation collection classes (NSArray, NSDictionary, NSSet, etc) don't do this because it's the correct way, but rather because it is the best compromise given the limitations of the language.

There are three basic problems to this approach.

  1. Your code is incapable of being self-documenting. It is impossible to know what is being held inside of an NSArray instance simply by knowing that is an NSArray. If we had an NSStringArray class we know just by looking at it that there are instances of NSString being contained by the array. This is the type of self-documenting behavior we want.

  2. Type errors are introduced at runtime instead of compile time. Because methods such as insertObject: take an id instead of an object of a specific type, the compiler will never complain about the type of object you pass it, even if you pass an object of a type you did not intend. This means you may not catch the error until the code is running. When writing large apps with complicated code paths it is very common for these sorts of errors not be caught right away. And it can be particularly hairy when refactoring your code, especially when there are some infrequent code paths that you will inevitably forget about.

  3. It increases the amount of state in your application. When the compiler cannot dictate the correctness of the types in your code it becomes your responsibility. You must write code to modify the behavior of your logic based on the type of a particular object. Your application's flow becomes dependent on you maintaining the correctness of this logic without the help of the compiler to tell you when you screw up. Like runtime errors, this becomes a bigger issue while refactoring.

It isn't very difficult to understand why lacking type information is problematic. But how can we fix this? And how do generics fit into the equation?

Well, we fix these problems by giving our objects types. We simply have to compose types in order to create ones that are appropriate for our objects' requirements.

We can do this in Swift by utilizing generics and abstracting out a common interface for the objects we want to handle similarly.

As a concrete example, let's consider an online store app that displays a list of available products.

We could create a type for a Product itself.

struct Product {
	let name: String
	let price: Double
}

But this can quickly become out of hand as you introduce different types of pruducts. Perhaps your store sells bikes and you'd like to have your user interface reflect the type of bike it is: a road bike, a mountain bike, a hybrid commuting bike, etc.

Now we have:

struct Product {
	enum BikeType {
        case Road
        case Mountain
        case Hybrid
    }

	let name: String
	let price: Double
	let bikeType: BikeType
}

Your store also sells skateboards and you'd like to display the width of the deck, so you'll need to store that information as well.

struct Product {
	enum BikeType {
        case Road
        case Mountain
        case Hybrid
    }
    
	let name: String
	let price: Double
	let bikeType: BikeType
	let deckWidth: Double
}

You can see that this Product type is quickly becoming out of hand and becoming responsible for more information than the type was originally meant to represent.

The next obvious step is to create types for each of different products.

struct Bike {
    enum Type {
        case Road
        case Mountain
        case Hybrid
    }

	let name: String
	let price: Double
	let type: Type
}

struct Skateboard {
	let name: String
	let price: Double
	let deckWidth: Double
}

But now we're back where we started and we're duplicating information. Both of these types have a name and price property. Additionally, we can no longer store objects that may be of either type in one collection.

Both of these problems can be solved by parameterization of the common properties between these two products.

protocol Purchasable {
	var name: String { get }
	var price: Double { get }
}

struct Bike : Purchasable {
    enum Type {
        case Road
        case Mountain
        case Hybrid
    }
    
    let name: String
    let price: Double
	let type: Type
}

struct Skateboard : Purchasable {
    let name: String
    let price: Double
	let deckWidth: Double
}

Doing this now gives us a new type, Purchasable, which we can use to refer to any object whose type implements the Purchasable protocol.

Using generics, we can now create an array whose type only allows objects that are Purchasable, in our application they can be either Bikes or Skateboards.

let bike = Bike(name: "Trek Explorer", price: 500.00, type: .Mountain)
let skateboard = Skateboard(name: "Generic Deck", price: 25.00, deckWidth: 8)

let products: [Purchasable] = [ bike, skateboard ]

As you can see, using generics we were able to create a whole new type, [Purchasable] (or with the alternate syntax: Array<Purchasable>), to represent an array of objects that are Purchasable. We immediately know looking at the type what it is and what we're holding inside of it. The Swift Array class abstracted out its logic using generics and now we get the perks of having specific array classes for a specific type (like the NSStringArray mentioned earlier) without having to write a new class manually for each type.

Additionally, this allows us to write code that restricts its own exposure to type information, limiting its responsibility to subset of the information that it actually cares about. For example, if we were to write a function that calculates the tax for a particular set of products, the function does not need to know anything about the product other than its price and therefore that is the only information the code using it should be exposed to. Because we have separated the price property into a protocol we can do so without our function caring whether the product is a Bike or a Skateboard.

func totalTaxForProducts(products: [Purchasable], taxRate: Double) -> Double {
        return taxRate * products.reduce(0.0) { sum, p in return sum + p.price }
}

let totalTax = totalTaxForProducts(products, 0.07)

Another type of problem we often use id to solve in Objective-C is the case where an object can be of multiple types and the instances aren't known at compile time. For example, consider a JSON object. Let's say we working with JSON objects that can be strings, integers, or booleans. We need a way to encapsulate all of these types so that we can write code that works generically with "JSON objects".

In Swift we can do this by creating a JSONObject type that is the composition of it's possible subtypes (String, Int, Bool) combined using an Enum.

enum JSONObject {
    case StringLiteral(String)
    case Integer(Int)
    case Boolean(Bool)
}

Note that in Swift, the name of an Enum case cannot be the same as the type of its associated values, hence the more verbose names in this example.

This allows us to create objects whose type is JSONObject, but can represent specific values of other types without needing to explicity test which kind of value it is. More concretely, we are able to replace Objective-C code such as:

	id object = [self decodeJSONObject:someString];

	if([object isKindOfClass:[NSString class]])
	{
		// object is a string
	}
	else if([object isKindOfClass:[NSNumber class]])
	{
		// object is a number or boolean
	}

with Swift code like this:

    switch object {
        case .StringLiteral(let value):
        	// object is a string

        case .Integer(let value):
            // object is a number

        case .Boolean(let value):
        	// object is a boolean
    }

This is better because object is adhering to a contract that can be checked by the compiler. It has a strict set of value types it can represent and the code reflects that exactly.

There are other implicit benefits to using method as well. For example, if you were to add or remove cases from the Enum, the compiler would alert you that you need to update the behavior of the switch statement to reflect the change. In the Objective-C code above, if you modify the behavior of the application such that the object can never be a number, you now have dead code in your application increasing its technical debt.

Swift's generics, in combination with protocols, allow a new level of abstract that was previously unavailable to Objective-C developers. The most obvious benefit is the introduction of typed collections as a first class citizen in the language but as we've seen above they play in an important role in making your own code safer and more correct. Similarly, Enums are a powerful feature when working with objects that can be of multiple types while maintaining an acceptable level of type safety.

Overall, these are very welcome additions to our programming repertoire.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment