Swift classes, protocols, and enums

Almost all Swift applications will be object oriented. Chapter 1, Exploring Swift, and Chapter 2, Playing with Swift, both demonstrated functional and procedural Swift code. Classes such as Process from the CoreFoundation framework, and UIColor and UIImage from the UIKit framework were used to demonstrate how classes can be used in applications. This section describes how to create classes, protocols, and enums in Swift.

Classes in Swift

A class is created in Swift using the class keyword and braces are used to enclose the class body. The body can contain variables called properties as well as functions called methods, which are collectively referred to as members. Instance members are unique to each instance, while class members are shared between all instances of that class.

Classes are typically defined in a file named for the class; so a GitHubRepository class can be defined in the GitHubRepository.swift file. A new Swift file can be created by navigating to File | New | File… and selecting the Swift option under iOS:

class GitHubRepository {
  var id:UInt64 = 0
  var name:String = ""
  func detailsURL() -> String {
    return "https://api.github.com/repositories/(id)"
  }
}

This class can be instantiated and used as follows:

var repo = GitHubRepository()
repo.id = 1
repo.name = "Grit"
repo.detailsURL() // returns https://api.github.com/repositories/1

It is possible to create class members, which are the same for all instances of a class. In the GitHubRepository class, the api URL is likely to remain the same for all invocations, so it can be refactored into a class property:

class GitHubRepository {
  // does not work in Swift 1.0 or 1.1
  class let api = "https://api.github.com"
  …
  func detailsURL() -> String {
    return "(api)/repositories/(id)"
  }
}

Now, if the api URL needs to be changed (for example, to support mock testing or to support an in-house GitHub Enterprise server), there is a single place to change it.

Note

In Xcode 6.1 with Swift 1.1, an error message the class variables are not yet supported may be seen.

To use class variables in Swift 1.1, a different approach must be used. It is possible to define computed properties, which are not stored but are calculated on demand. These have a getter (also known as an accessor) and optionally a setter (also known as a mutator). The previous example can be rewritten as:

class GitHubRepository {
  class var api:String {
    get {
      return "https://api.github.com"
    }
  }
  func detailsURL() -> String {
    return "(GitHubRepository.api)/repositories/(id)"
  }
}

Although this is logically a read-only constant (there is no associated set block), it is not possible to define let constants with accessors.

To refer to a class variable, use the type name—which in this case is GitHubRepository. When the expression GitHubRepository.api is evaluated, the body of the getter is called.

Subclasses and testing in Swift

A simple Swift class with no explicit parent is known as a base class. However, classes in Swift frequently inherit from another class by specifying a superclass. The syntax for this is class SubClass:SuperClass{...}.

Tests in Swift are written using the XCTest framework, which is included by default in Xcode templates. This allows an application to have tests written and then executed in place to confirm that no bugs have been introduced.

Tip

XCTest replaces the previous testing framework OCUnit.

The XCTest framework has a base class called XCTestCase that all tests inherit from. Methods beginning with test (and that take no arguments) in the test case class are invoked automatically when the tests are run. Test code can indicate success or failure by calling the XCTAssert* functions, such as XCTAssertEquals and XCTAssertGreaterThan.

Tests for the GitHubRepository class conventionally exist in a corresponding GitHubRepositoryTest class, which will be a subclass of XCTestCase. It can be implemented as:

import XCTest
class GitHubRepositoryTest: XCTestCase {
  func testRepository() {
    var repo = GitHubRepository()
    repo.id = 1
    repo.name = "Grit"
    XCTAssertEqual(
      repo.detailsURL(),
      "https://api.github.com/repositories/1",
      "Repository details"
    )
  }
}

Make sure that the GitHubRepositoryTest class is added to the test target by selecting the file and pressing Command + Option + 1 to show the File Inspector. The checkbox next to the test target should be selected. Tests should never be added to the main target. The GitHubRepository class should be added to both targets:

Subclasses and testing in Swift

When the tests are run by pressing Command + U or by navigating to Product | Test, the results of the test will be shown. Changing either the implementation or the expected test result will demonstrate whether the test is being executed correctly.

Tip

Always check whether a failing test causes the build to fail; this will confirm that the test is actually being run. For example, in the GitHubRepositoryTest class, modify the URL to remove https from the front and check whether a test failure is shown. There is nothing more useless than a correctly implemented test that never runs.

Protocols in Swift

A protocol is similar to an interface in other languages; it is a named type that has method signatures but no method implementations. Classes can implement zero or more protocols; when they do, they are said to adopt or conform to the protocol. A protocol might have a number of methods that are either required (the default) or optional (marked with the optional keyword).

Note

Optional protocol methods are only supported when the protocol is marked with the @objc attribute. This declares that the class will be backed by an NSObject class for interoperability with Objective-C. Pure Swift protocols cannot have optional methods.

The syntax for defining a protocol looks like:

protocol GitHubDetails {
  func detailsURL() -> String
  // protocol needs @objc if using optional protocols
  // optional doNotNeedToImplement()
}

Note

Note that protocols cannot have functions with default arguments. Protocols can be used against the struct, class, and enum types unless the @objc class attribute is used, in which case they can only be used against Objective-C classes or enums.

Classes conform to protocols by listing the protocol names after the class name, similar to a superclass.

Tip

When a class has both a superclass and one or more protocols, the superclass should be listed first.

class GitHubRepository: GitHubDetails {
  func detailsURL() -> String {
    // implementation as before
  }
}

The GitHubDetails protocol can be used as a type in the same places as an existing Swift type, such as a variable type, method return type, or argument type.

Note

Protocols are widely used in Swift to allow callbacks from frameworks that would otherwise not know about specific callback handlers. If a superclass was required instead, then a single class could not be used to implement multiple callbacks. Common protocols include UIApplicationDelegate, Printable, and Comparable.

Enums in Swift

The final concept to understand in Swift is enumeration, or enum for short. An enum is a closed set of values, such as North, East, South, and West or Up and Down.

An enumeration is defined using the enum keyword followed by a type name and a block, which contains case keywords followed by comma-separated values:

enum Suit {
  case Clubs, Diamonds, Hearts // many on one line
  case Spades // or each on separate lines
}

Unlike C, enumerated values do not have a specific type by default, so they cannot be converted to and from an integer value. Enumerations can be defined with raw values that allow conversion to and from integer values. Enum values are assigned to variables using the type name and the enum name:

var suit:Suit = Suit.Clubs

However, if the type of the expression is known, then the type prefix does not need to be explicitly specified. So the following form is much more common in Swift code:

var suit:Suit = .Clubs

Raw values

For enum values that have specific meanings, it is possible to extend the enum from a different type, such as Int. These are known as raw values.

enum Rank: Int {
  case Two = 2, Three, Four, Five, Six, Seven, Eight, Nine, Ten
  case Jack, Queen, King, Ace
}

A raw value enum can be converted to and from its raw value with the rawValue property and the failable initializer Rank(rawValue:) as follows:

Rank.Two.rawValue == 2
Rank(rawValue:14)! == Rank.Ace

Tip

Note that the failable initializer returns an optional enum value, because the equivalent Rank might not exist. The expression Rank(rawValue:0) will return nil, for example.

Associated values

Enums can also have associated values, such as a value or case class in other languages. For example, a combination of a Suit and a Rank can be combined to form a Card:

enum Card {
  case Face(Rank, Suit)
  case Joker
}

Instances can be created by passing values into an enum initializer:

var aceOfSpades: Card = .Face(.Ace,.Spades)
var twoOfHearts: Card = .Face(.Two,.Hearts)
var theJoker: Card = .Joker

The values of an enum cannot be extracted (as they can with a struct), but the enum value can be accessed by pattern matching in a switch statement:

var card = aceOfSpades // or theJoker or twoOfHearts ...
switch card {
  case .Face(var rank, var suit): 
    println("Got a face card (rank) of (suit)");
  case .Joker: 
    println("Got the joker card")
}

The Swift compiler will ensure that the switch statement is exhaustive. Since the enum only contains these two types, no default block is needed. If another enum value is added to Card in the future, the compiler will report an error in this switch statement.

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

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