In most languages, enumerations are little more than a data type consisting of a set of named values called elements. In Swift, however, the enumerations have been supercharged to give them significantly more power. Enumerations in Swift are a lot closer in functionality to classes and structures; however, they can still be used like enumerations in other languages.
Before we see how enumerations are supercharged in Swift, let's see how we can use them as standard enumerations. The following code defines an enumeration called Devices
:
enum Devices { case IPod case IPhone case IPad }
In the Devices
enumeration, we defined three possible values: IPod
, IPhone
, and IPad
.
One of the items that make enumerations different in Swift as compared to other languages is that they can be prepopulated with values known as raw values. As an example, we could redefine our Devices
enumeration to be prepopulated with String values, as shown in the following example:
enum Devices: String { case IPod = "iPod" case IPhone = "iPhone" case IPad = "iPad" }
We can then use the rawValue
property to retrieve the raw value for any of the enumeration's elements, as shown in the following code:
Devices.IPod.rawValue
In Swift, we can also store the associated values alongside our case values. These associated values can be of any type and can vary each time we use the case. This enables us to store additional custom information with our case types. Let's see how this works by redefining our Devices
enumeration with the associated values:
enum Devices { case IPod(model: Int, year: Int, memory: Int) case IPhone(model: String, memory: Int) case IPad(model: String, memory: Int) }
In the previous example, we defined three associated values with the IPod
case and two associated values with the IPhone
and IPad
cases. We can then use this new Devices
enumeration with the associated values, as follows:
var myPhone = Devices.IPhone(model: "6", memory: 64) var myTablet = Devices.IPad(model: "Pro", memory: 128)
In this example, we defined the myPhone
device as an iPhone 6 with 64 GB of memory and the myTablet
device as i
Pod Pro with 128 GB of memory. We can now retrieve the associated values as follows:
switch myPhone { case .IPod(let model, let year, let memory): print("iPod: \(model) \(memory)") case .IPhone(let model, let memory): print("iPhone: \(model) \(memory)") case .IPad(let model, let memory): print("iPad: \(model) \(memory)") }
In this example, we will simply print out the associated values of the myPhone
device.
What we have seen so far makes enumerations far more powerful than enumerations in other languages. However, we are not done showing off what enumerations can do in Swift. In Swift, enumerations are not limited to a list of elements. They can also contain computed properties, initializers, and methods just like classes and structures.
Let's take a look at how we can use methods and computed properties with enumerations. Since it almost feels like Christmas with all of these exciting new features, our example will take a bit of a holiday theme:
enum Reindeer: String { case Dasher, Dancer, Prancer, Vixen, Comet, Cupid, Donner, Blitzen, Rudolph static var allCases: [Reindeer] { return [Dasher, Dancer, Prancer, Vixen, Comet, Cupid, Donner, Blitzen, Rudolph] } static func randomCase() -> Reindeer { let randomValue = Int( arc4random_uniform( UInt32(allCases.count) ) ) return allCases[randomValue] } }
In this example, we created an enumeration called Reindeer
that contains the names of Santa's nine reindeers (we cannot forget Rudolph you know). Within the Reindeer
enumeration, we created an allCases
computed property that returns an array containing all of the possible cases for the enumeration. We also created a randomCase()
method that will return a random reindeer from our enumeration.
The previous examples showed how to use the individual features of Swift's enumerations, but what really makes them powerful is when we use these features together. Let's take a look at one more example where we combine the associated values with methods and properties to make a supercharged enumeration. We will start off by defining a basic enumeration that defines the various formats of a book with the page count and the price of each format stored in an associated value:
enum BookFormat { case PaperBack (pageCount: Int, price: Double) case HardCover (pageCount: Int, price: Double) case PDF (pageCount: Int, price: Double) case EPub (pageCount: Int, price: Double) case Kindle (pageCount: Int, price: Double) }
This enumeration would work great, but there are some basic drawbacks. The first one, and the one that really drives me nuts, is retrieving the associate values from our enumerations. For example, let's create the following instance:
var paperBack = BookFormat.PaperBack(pageCount: 220, price: 39.99)
Now, to retrieve the page count and the price of this enumeration, we could use the following code:
switch paperBack { case .PaperBack(let pageCount, let price): print("\(pageCount) - \(price)") case .HardCover(let pageCount, let price): print("\(pageCount) - \(price)") case .PDF(let pageCount, let price): print("\(pageCount) - \(price)") case .EPub(let pageCount, let price): print("\(pageCount) - \(price)") case .Kindle(let pageCount, let price): print("\(pageCount) - \(price)") }
This is quite a bit of code to retrieve the associated values, especially where we may need to retrieve these values in multiple locations throughout our code. We could create a global function that would retrieve these values, but we have a better way in Swift. We can add a computed property to our enumeration that will retrieve the pageCount
and price
values of our enumeration. The following example shows how we could add these computed properties:
enum BookFormat { case PaperBack (pageCount: Int, price: Double) case HardCover (pageCount: Int, price: Double) case PDF (pageCount: Int, price: Double) case EPub (pageCount: Int, price: Double) case Kindle (pageCount: Int, price: Double) var pageCount: Int { switch self { case .PaperBack(let pageCount, _): return pageCount case .HardCover(let pageCount, _): return pageCount case .PDF(let pageCount, _): return pageCount case .EPub(let pageCount, _): return pageCount case .Kindle(let pageCount, _): return pageCount } } var price: Double { switch self { case .PaperBack(_, let price): return price case .HardCover(_, let price): return price case .PDF(_, let price): return price case .EPub(_, let price): return price case .Kindle(_, let price): return price } } }
With these computed properties, we can very easily retrieve the associated values of our BookFormat
enumeration. The following code demonstrates how to use them:
var paperBack = BookFormat.PaperBack(pageCount: 220, price: 39.99) print("\(paperBack.pageCount) - \(paperBack.price)")
These computed properties hide the complexity of the switch
statement and give us a much cleaner dot syntax to use. We can also add methods to our enumerations. Let's say, as an example, that if a person buys multiple copies of our book in different formats, they would receive a 20% discount. The following function could be added to our BookFormat
enumeration to calculate this discount:
func purchaseTogether(otherFormat: BookFormat) -> Double { return (self.price + otherFormat.price) * 0.80 }
We could now use this method as shown in the following code:
var paperBack = BookFormat.PaperBack(pageCount: 220, price: 39.99) var pdf = BookFormat.PDF(pageCount: 180, price: 14.99) var total = paperBack.purchaseTogether(pdf)
As we can see, enumerations in Swift are a lot more powerful than enumerations in other languages. The one thing to avoid is overusing the enumeration. They are not meant to be a replacement for either the class or the structure. Deep down enumerations are still a data type consisting of a finite set of named values and all of these new exciting features are there to make them more useful to us.
When we create instances of the enumeration, it is named therefore it is a named type. The enumeration type is also a value type. Now, let's look at one of the most underutilized types in Swift, the Tuple type.