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.
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.
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.
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.
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:
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.
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.
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).
The syntax for defining a protocol looks like:
protocol GitHubDetails { func detailsURL() -> String // protocol needs @objc if using optional protocols // optional doNotNeedToImplement() }
Classes conform to protocols by listing the protocol names after the class name, similar to a superclass.
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.
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
.
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
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
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.