Enumerations

So far, we have covered two of the three types of classification in Swift: structure and class. The third classification is called enumeration. Enumerations are used to define a group of related values for an instance. For example, if we want values to represent one of the three primary colors, an enumeration is a great tool.

Basic declaration

An enumeration is made up of cases much like a switch and uses the keyword enum instead of struct or class. An enumeration for primary colors should look like this:

enum PrimaryColor {
    case Red
    case Green
    case Blue
}

You can then define a variable with this type and assign it one of the cases:

var color = PrimaryColor.Green

Note that, to use one of the values, we must use the name of the type followed by a dot (.) and then the specific case. If the type of the variable can be inferred, you can even leave out the enumeration name and just start with a dot:

var color = PrimaryColor.Green
color = .Red

During the assignment to .Red, the compiler already knows that the color variable is of the type PrimaryColor so it doesn't need us to specify that again. This is a great way of making your code more concise but make sure you don't sacrifice legibility. If you leave out the type name, it should still be obvious from the context of the code.

Testing enumeration values

Enumeration instances can be tested for a specific value as with any other type, using the equality operator (==):

if color == PrimaryColor.Red {
}
else if color == .Blue {
}

Note that, in the second if statement, where color is checked for if it is blue, the code takes advantage of type inference and doesn't bother specifying PrimaryColor.

This method of comparison is familiar and useful for one or two possible values. However, there is a better way to test an enumeration for different values. Instead of using an if statement, you can use a switch. This is a logical solution considering that enumerations are made up of cases and switches test for cases:

switch color {
    case .Red:
        print("color is red")
    case .Green:
        print("color is green")
    case .Blue:
        print("color is blue")
}

This is great for all the same reasons that switches themselves are great. In fact, switches work even better with enumerations because the possible values for an enumeration are always finite, unlike other basic types. You may remember that switches require that you have a case for every possible value. This means that, if you don't have a test case for every case of the enumeration, the compiler will produce an error. This is usually great protection and that is why I recommend using switches rather than simple if statements in most circumstances. If you ever add additional cases to an enumeration, it is great to get an error everywhere in your code that doesn't consider that new case so that you make sure you address it.

Raw values

Enumerations are great because they provide the ability to store information that is not based on the basic types provided by Swift such as strings, integers, and doubles. There are many abstract concepts like our color example, that are not at all related to a basic type. However, you often want each enumeration case to have a raw value that is another type. For example, if we wanted to represent all of the coins in United States currency along with their monetary value, we could make our enumeration have an integer raw value type, like this:

enum USCoins: Int {
    case Quarter = 25
    case Dime = 10
    case Nickel = 5
    case Penny = 1
}

The raw value type is specified in the same way that inheritance is specified with classes and then each case is individually assigned a specific value of that type.

You can access the raw value of a case at any time by using the rawValue property:

print("A Quarter is worth (USCoins.Quarter.rawValue) cents.")

Keep in mind that an enumeration can only have raw value types that can be defined with literals like 10, or String. You cannot define an enumeration with your own custom type as its raw value.

Associated values

Raw values are great for when every case in your enumeration has the same type of value associated with it and its value never changes. However, there are also scenarios where each case has different values associated with it and those values are different for each instance of the enumeration. You may even want a case that has multiple values associated with it. To do this, we use a feature of enumerations called associated values.

You can specify zero or several types to be associated separately with each case with associated values. Then, when creating an instance of the enumeration, you can give it any value you want, as shown:

enum Height {
    case Imperial(feet: Int, inches: Double)
    case Metric(meters: Double)
    case Other(String)
}
var height1 = Height.Imperial(feet: 6, inches: 2)
var height2 = Height.Metric(meters: 1.72)
var height3 = Height.Other("1.9 × 10-16 light years")

Here, we have defined an enumeration to store a height measurement using various measurement systems. There is a case for the imperial system that uses feet and inches and a case for the metric system that is in just meters. Both of these cases have labels for their associated values which are similar to a tuple. The last case is there to illustrate that you don't have to provide a label if you don't want to. It simply takes a string.

Comparing and accessing values of enumerations with associated values is a little bit more complex than for regular enumerations. We can no longer use the equality operator (==). Instead, we must always use a case. Within a case, there are multiple ways that you can handle the associated values. The easiest thing to do is to access the specific associated value. To do that, you can assign it to a temporary variable:

switch height1 {
    case .Imperial(let feet, var inches):
        print("(feet)ft (inches)in")
    case let .Metric(meters):
        print("(meters) meters")
    case var .Other(text):
        print(text)
}

In the imperial case, the preceding code assigned feet to a temporary constant and inches to a temporary variable. The names match the labels used for the associated values but that is not necessary. The metric case shows that, if you want all of the temporary values to be constant, you can declare let before the enumeration case. No matter how many associated values there are, let only has to be written once instead of once for every value. The other case is the same as the metric case except that it creates a temporary variable instead of a constant.

If you wanted to create separate cases for conditions on the associated values, you could use the where syntax that we saw in the previous chapter:

switch height1 {
    case .Imperial(let feet, var inches) where feet > 1:
        print("(feet)ft (inches)in")
    case let .Metric(meters) where meters > 0.3:
        print("(meters) meters")
    case var .Other(text):
        print(text)
    default:
        print("Too Small")
}

Note that we had to add a default case because our restrictions on the other cases were no longer exhaustive.

Lastly, if you don't actually care about the associated value, you can use an underscore (_) to ignore it, as shown:

switch height1 {
    case .Imperial(_, _):
        print("Imperial")
    case .Metric(_):
        print("Metric")
    case .Other(_):
        print("Other")
}

This shows you that, with enumerations, switches have even more power than we saw previously.

Now that you understand how to use associated values, you might have noticed that they can change the conceptual nature of enumerations. Without associated values, an enumeration represents a list of abstract and constant possible values. An enumeration with associated values is different because two instances with the same case are not necessarily equal; each case could have different associated values. This means that the conceptual nature of enumerations is really a list of ways to look at a certain type of information. This is not a concrete rule but it is common and it gives you a better idea of the different types of information that can best be represented by enumerations. It will also help you make your own enumerations more understandable. Each case could theoretically represent a completely unrelated concept from the rest of the cases using associated values but that should be a sign that an enumeration may not be the best tool for that particular job.

Methods and properties

Enumerations are actually very similar to structures. As with structures, enumerations can have methods and properties. To improve the Height enumeration, we could add methods to access the height in any measurement system we wanted. As an example, let's implement a meters method, as follows:

enum Distance {
    case Imperial(feet: Int, inches: Double)
    case Metric(meters: Double)

    func meters() -> Double {
        switch self {
            case let .Imperial(feet, inches):
                return Double(feet)*0.3048+inches*0.3048/12
            case let .Metric(meters):
                return meters
        }
    }
}
var distance1 = Distance.Imperial(feet: 6, inches: 2)
distance1.meters() // 1.8796

In this method, we have switched on self which tells us which unit of measurement this instance was created with. If it is in meters we can just return that but, if it is in feet and inches, we must do the conversion. As an exercise, I recommend you try to implement a feetAndInches method that returns a tuple with the two values. The biggest challenge is in handling the mathematical operations using the correct types. You cannot perform operations with mismatching types mathematically. If you need to convert from one number type to another, you can do so by initializing a copy as shown in the code above: Double(feet). Unlike the casting that we discussed earlier, this process simply creates a new copy of the feet variable that is now Double instead of Int. This is only possible because the Double type happens to define an initializer that takes Int. Most number types can be initialized with any of the other ones.

You now have a great overview of all of the different ways in which we can organize Swift code in a single file to make the code more understandable and maintainable. It is now time to discuss how we can separate our code into multiple files to improve it even more.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset