© Radoslava Leseva Adams and Hristo Lesev 2016

Radoslava Leseva Adams and Hristo Lesev, Migrating to Swift from Flash and ActionScript, 10.1007/978-1-4842-1666-8_21

21. Object-Oriented Programming Topics

Radoslava Leseva Adams and Hristo Lesev2

(1)London, UK

(2)Kazanlak, Bulgaria

In this chapter we will go over the object-oriented side of Swift. Having experience with ActionScript, you are already familiar with the object-oriented programming (OOP) paradigm. It treats a piece of software as a system, which is broken down into objects. Each object is responsible for a part of the system and looks after its own state.

Swift uses four main OOP entities:

  • Classes

  • Structures

  • Enumerations

  • Protocols

Each of these entities defines a type (classes, structures, and enumerations) or part of a type (protocols). Classes in Swift are very similar to classes in ActionScript. A lot of the techniques that you will need to know when you define and use classes apply to structures and enumerations too. Protocols serve similar purpose as interfaces do in ActionScript: they define a way of communicating with a set of types by requiring each of those types to conform to a set of requirements. Implementing these requirements is called conforming and is typically left to the types that adopt the protocol.

We will have a detailed look at how classes work first. Then we will highlight what makes structures different from classes and when it is preferable to use one or the other. We will also see how protocols are defined and adopted.

Enumerations only get a mention in this chapter. As they may be new to an ActionScript developer, enumerations have a dedicated in-depth section in Chapter 22 , which deals with new and different concepts.

Classes

A class provides a mold, from which objects (class instances) are made. This is true for structures and enumerations too: each of these entities defines placeholders for data (properties) and logic for working with data (methods and subscripts). Most of the ideas and techniques that will help you define and use classes in Swift are valid for structures and enumerations. To avoid duplicating information, we will present classes in detail and will point out the similarities and the differences that you need to be aware of when using structures and enumerations. Where something applies to all three entities, we will refer to them as types for conciseness.

Syntax for Defining a Class

The way you define a class in Swift is very similar to how it is done in ActionScript: you use the class keyword, followed by the name of the class, followed by the definitions of the class’s members in curly brackets: properties, methods, and subscripts. Let us define a class, called File, which we will add to later to make a very simplistic representation of an iOS file (Listing 21-1).

Listing 21-1. Class Declaration Syntax
class File {
    // add class members here
}

Access and Visibility

In ActionScript we are used to putting each class in its own file with a matching name and to defining packages for accessing classes. For example, to access a class called MyClass, which has been defined as public or internal in the com.diadraw package, you need either to import com.diadraw in the files where you want to use MyClass or to access it as com.diadraw.MyClass.

In Swift you don’t have to define classes in separate files and you are not forced to name source files after your classes. As we saw in the section “Access Control” in Chapter 17 , visibility is defined in terms of source files and modules, where a module is a collection of source files that you build and ship together either as an application or as a framework. The control modifiers we introduced—public, internal, and private—can be used to control access to whole classes, structures, enumerations, and protocols, as well as to their individual members:

  • An entity marked as private is only visible to code in the same source file.

  • The default level is internal and makes an entity visible within the same module. You don’t have to use the internal modifier explicitly: unless you put another modifier, internal is assumed.

  • Mark your entities as public to make them visible to external code that may use your module.

Access modifiers go before the class definition, as shown in Listing 21-2.

Listing 21-2. Declaring a Private Class
private class MyPrivateClass {
    // add class members here
}

Note that to make a class or a structure public, you need to define at least one public initializer for it or, in ActionScript words, it needs to have a public constructor. More on initializers later.

Adding Properties

Properties are defined in the body of the type (i.e., inside the curly brackets), a syntax that should look familiar from ActionScript.

Like ActionScript, Swift lets you define type properties, marked with the static keyword and instance properties. A member marked static is part of the type, rather than of an instance of the type, and there will be only one copy of it, no matter how many instances you create.

For class members there is another keyword, class, which is used to declare type properties in a very specific situation: when you want a computed property to be overridden in subclasses, you need to mark it with the class keyword, instead of with static. We will see what computed properties are later.

In terms of how property values are retrieved, Swift distinguishes between three kinds of properties: stored, computed, and lazy. We will look at each of these in detail.

Stored Properties

Stored propertieshave their values kept for immediate retrieval in variables or constants: you declare a variable property with the var keyword and a constant property with the let keyword, like we saw in Chapter 17 . You can provide an initial value for a stored property as part of its declaration: this is its default value.

Let us add a couple of stored properties to the class File we declared earlier: we will have a Boolean property, called isOpen to indicate whether the file is currently open and a type property: numberOfOpenFiles, of type Int, which will keep track of how many files are currently open in the file system. It will have the same value for every instance of File (see Listing 21-3).

Listing 21-3. Adding Stored Properties
class File {
    // The instance property isOpen
    // has a different value for every instance of File:    
    var isOpen = false


    // The type property numberOfOpenFiles
    // has the same value for every instance of File:
    private static var numberOfOpenFiles = 0
}

Note that the type inference we talked about in Chapter 19 applies here too: if you provide default values for your stored properties, the property types will be inferred from these values. Thus, instead of defining var isOpen: Boolean = false, we do just var isOpen = false.

Computed Properties

Computed propertiesare not necessarily backed up with a constant or a variable, but provide getter functions, which work out the property’s value at runtime. They can also have optional setter functions. Because you don’t supply default values for computed properties, their types are not automatically inferred and you need to explicitly state the type in the property declaration.

To see what this looks like, we will first add another stored property to the File class: let us call this location and have it store the full path to the file as an NSURL object. The NSURL class is part of the iOS SDK. It stores locations of resources or paths to local files and is handy for parsing these paths. It has similarities with the URLRequest and File classes in ActionScript.

Next we will define a property name, which will be computed at runtime, using the value of location and will let us read or change only the file name. A computed property is declared as a variable followed by curly brackets: inside the curly brackets we define a getter and an optional setter. Because the getter and the setter functions are defined within the definition of the property, they don’t need elaborate signatures: the type of the value they return or receive is inferred from the type of name.

I will confess in advance that the code in Listing 21-4 will not compile at this stage: the Swift compiler comes with the wonderful1 feature of not letting you leave anything uninitialized, and if you look at the location property, you will notice that we have not provided an initial value for it. More on initialization later.

Listing 21-4. Adding Computed Properties
class File {
    // The rest of the class definition stays the same


    // This property stores the full path and file name:
    var location: NSURL


    // The file name is computed at runtime
    var name: String {
        get {
            return (location.lastPathComponent)!
        }


        set (newFileName) {
            let newUrl = location.URLByDeletingLastPathComponent
            location = newUrl!.URLByAppendingPathComponent(newFileName)
        }
    }
}

We can make the setter function even more concise: if we omit its parameter name, the parameter will not disappear but will be available to us with the implied name of newValue. So we can rewrite the setter like the example in Listing 21-5 shows.

Listing 21-5. Using the Implied Parameter newValue from a Setter Function
set {
    let newUrl = location.URLByDeletingLastPathComponent
    location = newUrl!.URLByAppendingPathComponent(newValue)
}

Lazy Properties

Lazy propertiesare variable properties, which are marked with the lazy keyword. They are similar to stored properties in that they store their values. The initial value of a lazy property is usually the result of a function call, which is only made when the property is accessed for the first time.

Declaring a property as lazy can save unnecessary overhead, if the property’s initial value requires heavy computation and there is a chance that it will never be needed, for example. The flipside of this is that you are not in control of when initialization happens and it can potentially hurt performance at an inopportune time. This also leads to a couple of formalities that the compiler will remind you of, if you forget.

  • A lazy property can only be a variable and not a constant, so you must declare it with the var keyword.

  • You must provide a default value: this will typically be a call to a function or an initializer that will provide an initial value for the property.

Let us add a property to the File class that will give us access to its contents. As this might require loading of a lot of data and may never be needed in the lifespan of a File instance, it is a good candidate for a lazy property. We will also add a function, called loadContent that will load a file’s contents, given its path (Listing 21-6).

Listing 21-6. Adding Computed Properties
func loadContent(fromPath path: NSURL?) -> String? {
    // Potentially lenghty operation
    // ...
}


class File {
    // The rest of the class definition stays the same


    lazy var contentAsString: String? = self.loadContent(fromPath: self.location)
}

Getting Notified About Changes: Property Observers

When a property is set, it is useful to be able to react to the potential change. In Swift you have a couple of options for how and when to do that: you can declare a property setter, like we saw with computed properties earlier, or you can add property observers, which will fire just before and just after the property value is set. The observers are functions, called willSet and didSet, which you define in curly brackets after the declaration of the property you need to observe.

Here is something to keep in mind: you can’t define property observers for just any property in Swift. The observed property needs to be stored, non-lazy and variable, rather than constant. Following are the reasons:

  • Constant properties do not allow their values to be set, except inside an initializer method (constructor) or at the point where they are defined: at both points the containing type has full control over how the value is set, so firing observers would be unnecessary.

  • Lazy properties behave as if their initial values are set at the moment you request them to be set, but the actual setting may happen at a later point or not at all (see the start of the section “Properties,” on how lazy properties work).

  • You can define setter methods on computed properties: these give you control over the value of a property at the same points when willSet and didSet would fire.

Let us add observers to the isOpen property of our File class in order to see how they are defined. Listing 21-7 defines willSet and didSet functions inside curly brackets following the declaration of isOpen. In the willSet observer we will simply print what isOpen is about to be set to and in didSet we will increment or decrement the total number of open files.

Both willSet and didSet have implied parameters, called newValue and oldValue, respectively, which give you access to the value the property is about to be set to or the value it had before it was set. Instead of using newValue and oldValue, you can give the parameters custom names, which you put in brackets after the name of the observer (e.g., willSet(toValue)); the type of the parameter is inferred.

Listing 21-7. Adding Property Observers
class File {
    // The rest of the class definition stays the same


    var isOpen = false {
        willSet {
            print("About to set isOpen to (newValue)")
        }


        didSet {
            // If the value hasn't changed, we don't want to do anything:
            if oldValue == isOpen
            {
                return
            }


            // If the file was just open,
            // increment the total number of open files.
            // If the file was closed, decrement the total number.
            if isOpen {
                File.numberOfOpenFiles += 1
            }
            else {
                File.numberOfOpenFiles -= 1
            }
        }
    }
}

Accessing Properties

You access properties using the familiar dot syntax, prefixing a property with a name of an instance for instance properties (file.isOpen = true), or with the name of a type for static properties (File.numberOfOpenFiles = 12).

The rules for access control we saw in Chapter 17 apply to properties too: unless you specify an access modifier explicitly (public, internal, or private), properties are declared internal by default.

The Self Property

Every class, structure, or enumeration has a property, called self. It is implicit and you don’t have to declare it yourself. You can only access the self property of a type from inside one of its methods:

  • When accessed from an instance method, self gives you access to the instance. In value types like structures and enumerations you can make assignments to self—we will see how that works when we look at structures later on.

  • When accessed from a type method (marked as static), self refers to the type and can give you access to the type’s other properties and methods.

Adding Methods

In Swift you can add methods to classes, structures, and enumerations in a similar way you do in ActionScript: you define the method within the curly brackets of the type definition.

Differentiating between instance and type methods will also be familiar to you: to define a type method, you mark it with the static keyword. A method marked static will only have access to properties of the type marked as static or class and not to instance properties.

Let us add a few methods to the File class to demonstrate the syntax for method definition. Listing 21-8 defines two instance methods: these simply mark a file as open or closed. There is also a type method, which resets the total number of open files.

Listing 21-8. Adding Methods to a Class
class File {
    func open() {
        isOpen = true;
    }


    func close() {
        isOpen = false;
    }


    static func resetNumberOfOpenFiles() {
        numberOfOpenFiles = 0;
    }


    // The rest of the class definition comes here.
}

Overloading Methods

You can overload methods (i.e., provide several methods with the same name), but with different types or number of parameters, the way you can overload any function. Flip back to the introductory Chapter 17 to see examples.

Initializer and Deinitializer Methods

Initializer methodsin Swift are the equivalent of what you know as constructors in ActionScript. Instead of being named after the type they initialize, however, the syntax for defining an initializer is as follows: you use the keyword init, followed by zero or more arguments wrapped in parentheses, followed by curly brackets where the body of the method goes. Initializers can be overloaded: you can define several initializer methods, which have a different number or different types of parameters.

Deinitializer methodsonly apply to classes in Swift and do not have an ActionScript analogue. Although they are technically methods, you can’t call them directly. Instead, they are called automatically just before an instance of a class gets deallocated. This gives you a chance to clean up any resources that the instance might be using: close an open file, for example. To define a deinitializer, use the keyword deinit, followed by curly brackets, between which you write the logic of the method. Note that you don’t need parentheses after deinit.

Listing 21-9 adds an initializer and a deinitializer to the File class we defined at the start of this chapter. The initializer takes a single parameter, location, which provides an initial value for the location property of the class. Remember how we could not compile the code before? This initializer will make the compiler happy: now we do not have any uninitialized properties.

The deinitializer makes sure that a file we might have opened is closed before the instance of File gets deallocated.

Listing 21-9. Defining Initializers and a Deinitializer
class File {
    init(location: NSURL) {
        self.location = location
    }


    deinit {
        close()
    }


    // The rest of the class definition comes here.
}
Invoking Initializers

An initializer will be called at the point where you create an instance of a type. You have two options: you can call an initializer directly by prefixing it with the type name and a dot, like you would call any other method, for example:

File.init("Documents\textFile.txt")

You can also just use the type name, followed by parentheses, and pass in any arguments your chosen initializer takes, for example:

File ("Documents\textFile.txt")
Rules for Initialization

As we pointed out in the opening Chapter of 17 and have seen time and time again, Swift is all about being explicit but concise at the same time, and about taking maximum advantage of the compiler to protect you from unintentionally omitting or defining things badly. This is especially true for initialization.

There are rules you need to follow when you initialize your types, which we will mention in passing here. You can find the complete set of rules in The Swift Programming Language manual on Apple’s web site. You do not have to worry about remembering all of them: keeping in mind the goal behind the rules and letting the compiler guide you through what is allowed and what isn’t should stand you in good stead.

Initialization in Swift is designed so that no part of an instance is left in an undefined state. This means that all of the stored properties of that instance need to have an initial value by the time the instance has finished initializing. For class instances this includes stored properties that are inherited from base classes.

As in ActionScript you have two options for when a stored property is initialized. You can provide a default value at the point where you declare the property, as we saw in the section “Properties”: this is phase one of the initialization. You can also assign it an initial value in an initializer function: this is phase two of the initialization. Or you can do both.

Phase one and phase two of the initialization process are the only times when you are allowed to set the value of a constant stored property.

Following are a few main points to keep in mind:

  • All stored properties must have an initial value when an instance of a class, structure, or enumeration is created.

  • You can assign default values to stored properties where you declare them, but you don’t have to: any property without a default value must be assigned an initial value in an initializer function.

  • If all of the stored properties of an instance have default values, you don’t need to declare an initializer explicitly: a default initializer without parameters will be defined for you by the compiler behind the scenes.

  • You can define multiple initializers.

  • Initializers can call other initializers.

  • You can call a base class initializer from within a child class initializer by using super.init(/*parameters go here*/).

  • If you define an initializer that has the same signature as a superclass initializer, you are effectively overriding the superclass initializer, so you must prefix the child class one with the override keyword.

  • The Swift compiler distinguishes between two types of initializers, depending on whether they fully initialize an instance:

    • Designated initializers make sure that every stored property has an initial value.

    • Convenience initializers do not need to fully initialize an instance. Being able to write a convenience initializer means that you need not duplicate initialization code. However, in the spirit of making sure that the instance is fully initialized, a convenience initializer needs to call other initializers that will fill in the gaps. The rule is: in the chain of initializer calls there should be at least one designated initializer. You mark convenience initializers with the convenience keyword.

Listing 21-10 defines a class Directory with two stored properties—name, which is a String without a default value, and parentDirectory, which is an optional Directory with a default value of nil. Any initialization path we define will need to make sure that name receives an initial value.

Let us add two initializers: the first one takes no parameters and sets a new directory’s name to “unnamed directory.” The second one takes a reference to another Directory instance, with which it initializes the parentDirectory property. Since it does not assign a value to the name property, we need to mark this initializer with the convenience keyword and make it call the designated initializer method to make sure that all properties have initial values.

Listing 21-10. Designated and Convenience Intializers
class Directory {
    var name: String
    var parentDirectory: Directory?


    init() {
        name = "unnamed directory"
    }


    convenience init(parentDirectory: Directory) {
        self.init()
        self.parentDirectory = parentDirectory
    }
}
When Things Fail

You may remember from the section “Optionals” section in Chapter 19 that you need to tell the Swift compiler explicitly if a variable is allowed to have a value of nil by declaring it as an optional type. Similarly, when you have an initializer, which may fail to initialize an instance for any reason, you need to signal the compiler by declaring it as failable. You do this by adding a question mark after the init keyword: this will make the initializer create an optional type and return nil to fail.

When can initialization fail? Say one of the properties of your type needs to get its initial value from data, which you request over a network connection. The network connection falling apart, for example, would be a scenario you need to provide for.

To demonstrate this, let us modify the example from Listing 21-10 and make the second initializer in Directory check if the directory we are about to assign to parentDirectory exists and fail the initialization if it doesn’t. The modified initializer is marked as fallible by adding a question mark after init and by returning nil when conditions for initialization are not met (Listing 21-11).

Listing 21-11. Designated and Convenience Intializers
class Directory {
    var name: String
    var parentDirectory: Directory?


    func exists() -> Bool {
        // check if the directory exists
    }


    init() {
        name = "unnamed directory"
    }


    convenience init?(parentDirectory: Directory) {  
        guard parentDirectory.exists() else {
            return nil
        }


        self.init()
        self.parentDirectory = parentDirectory
    }
}
Note

Listing 21-11 uses a guard statement to ensure that conditions are met (a parent directory exists) before execution can proceed. We covered guard in Chapter 20.

Adding Subscripts

Subscripts are the third kind of member you can add to Swift types. A subscript lets you access a type’s data with an index, a key, or even a list of keys in square brackets, much like you would query an array or a dictionary. For example, file[10..100] could be made to retrieve a range of bytes from a file. Subscripts get their own detailed section: for how to define and use them see Chapter 22 .

Memory Management for Classes: ARC

Unlike in ActionScript, where all types are passed by reference, in Swift there are different memory management rules for classes, structures, and enumerations. In this section we will go over the rules that apply to classes and later, when we introduce structures and enumerations, we will see the differences in memory management for these types.

Reference types

Classes in Swift are reference types. In other words, if you create an instance of an object, assign it to a variable (or a constant) and then assign that variable to a second variable, instead of two copies of the same instance, you end up with one instance and two references to it: one in each variable. When you make changes to the object using one reference, the same changes will be accessible via the second reference.

Listing 21-12 shows this in practice: it has a definition of a class with a single property name of type String. We create an instance of this class and assign it to a variable named originalReference, after which we assign originalReference to a variable of the same type, named secondReference. Now, if we change the name property of one of the references, we would expect to see the name property of the other reference change too.

Listing 21-12. Assigning Reference Type Instances
class SitcomCharacter {
    var name: String


    init (name: String) {
        self.name = name
    }
}


// We create a new instance of SitcomCharacter,
// which originalReference refers to:
var originalReference = SitcomCharacter(name: "Lister")


// secondReference refers tot he same instance as originalReference:
var secondReference = originalReference


// Changing a property via one of the references changes the instance:
secondReference.name = "David Lister"


// We can see the change reflected in the gamePlayerB reference:
print(originalReference.name) // prints out "David Lister"

The same thing happens when you pass a class instance as an argument to a function: the function argument creates another reference to the same instance, instead of another copy of it.

Note

This only applies to classes in Swift. Structures and enumerations obey different rules—read on to find out.

To manage the memory taken by class instances, Swift uses Automatic Reference Counting (ARC) . This means keeping track of the number of references a given instance has and automatically deallocating the memory it occupies once that count goes down to zero. For you as a developer, this means that you do not need to worry about manually freeing memory. Instead, what you need to pay attention to is the scope of each reference you declare and make sure references live long enough to serve their purpose, but no longer, so that they do not hog valuable memory.

Memory Leaks from Strong Reference Cycles

If you think that hogging memory for too long is hard to achieve with ARC, think again. Let us look at an example of two classes: Country and Capital, defined in Listing 21-13. Country has a property of type Capital? and Capital has a property of type Country?. The question marks in the declarations make both of these properties optional and also causes them to be initialized to nil, as we have not provided default values for them. We learned how optionals work in Swift in Chapter 19 ; flip back to it if you need a reminder.

Listing 21-13. Defining Types That Hold References to One Another
class Country {
    var name: String?


    // Country holds a reference to Capital:
    var capital: Capital?


    init(name: String) {
        self.name = name
    }


    deinit {
        print("Instance of Country is being deallocated.")
    }
}


class Capital {
    var name: String?


    // Capital holds a reference to Country:
    var country: Country?


    init(name: String) {
        self.name = name
    }


    deinit {
        print("Instance of Capital is being deallocated.")
    }
}

Next, let us create an instance of each class and assign the Country instance to the Capital’s country property and vice versa, as shown in Listing 21-14.

Listing 21-14. Creating a Strong Reference Cycle
var bulgaria: Country? = Country(name: "Bulgaria")
var sofia: Capital? = Capital(name: "Sofia")


bulgaria!.capital = sofia
sofia!.country = bulgaria

Now bulgaria.capital holds a reference to sofia and sofia.country holds a reference to bulgaria. As diligent programmers we want each of these instances to be deallocated when we are done with it, so we assign nil to each of the original references we declared (Listing 21-15).

Listing 21-15. Setting the Original References to Nil to Decrease the Reference Count
bulgaria = nil
sofia = nil

This is where we get into a chicken-and-egg situation. Each of our instances has two variables referencing it (i.e., two references): the variable we declared when creating the instance (bulgaria and sofia) and a property in the other class’s instance (sofia.country and bulgaria.capital) (see Figure 21-1). The line bulgaria = nil removes one reference from the reference count of the Country instance and we are left with one reference, held by sofia.country. The same thing happens to the Capital instance when we do sofia = nil. After these two lines we are left with an instance of Country and an instance of Capital and no way of accessing them in order to have the memory deallocated, hence a memory leak. You can verify this by executing the lines above and checking if anything gets printed in the console: if the Capital instance gets deallocated, for example, we would expect the print call we put in its deinitializer to get executed.

A371202_1_En_21_Fig1_HTML.jpg
Figure 21-1. A strong reference cycle

The problem we just saw is known as a strong reference cycle and Swift offers a couple of options for avoiding it. In order for a reference not to affect the reference count of an instance, you can mark it as weak or unowned. Following is a summary of the types of references you can use:

  • Strong. This is the default type of reference that you create. It doesn’t need any keywords and causes a reference to be counted toward the reference count of an instance. The instance will be deallocated when its strong reference count goes down to zero.

  • Weak. A weak reference is marked with the weak keyword and doesn’t count toward the references that ARC keeps track of. Weak references must be declared as optional types, as they are allowed to have no value (i.e., they can contain nil). ARC will automatically set a weak reference to nil when the instance it refers to gets deallocated. Because a week reference is allowed to change value (it will when it is automatically set to nil), you can’t use it with constants.

  • Unowned. An unowned reference is marked with the unowned keyword and it too will not keep an instance alive when its strong reference count goes to zero. Unlike weak references, unowned ones cannot be optional types. This means that they can’t contain nil and will not automatically be set to nil when the instance they refer to gets deallocated.

Listing 21-16 shows the Capital class, now redefined to keep a weak reference to Country. This is enough to avoid the strong reference cycle we created earlier and to have both instances deallocated when you execute the lines from Listing 21-15.

Listing 21-16. Declaring a Weak Reference
class Capital {
    var name: String?


    // Capital holds a reference to Country:
    weak var country: Country?


    init(name: String) {
        self.name = name
    }


    deinit {
        print("Instance of Capital is being deallocated.")
    }
}

Comparing References

Swift distinguishes between comparison of class instances and comparison of references to class instances.

To be able to compare instances of the same class, the equality (==) and the inequality operator (!=) must be defined for that class: for example, they could do member-wise comparison of the two instances and declare them equal if all properties in both instances have the same value.

Comparing references is a different matter: when two variables refer to the same instance of a class, they are said to be identical. You can check if two references are identical with the identity operators: === and !==.

Extending Functionality

One of the principles of OOP is that entities should be “open for extension, but closed for modification.” In most other OOP languages this means that you can only extend a class or another entity’s functionality by inheriting it. Swift seemingly breaks the open-closed principle, though in a clever way: it allows you to add to existing entities, including SDK or third-party classes, structures, and so on, by writing extensions. 2 We will see how this is done later in this section.

Subclassing and Inheritance

Most of what you are used to when inheriting classes in ActionScript is true in Swift. Here we will point out a few things that are worth paying attention to, because they either work differently in Swift or build on ideas that ActionScript does not have.

Note

Subclassing only applies to classes. Structures and enumerations can’t inherit other structures or enumerations.

Following are the main guidelines for using inheritance in Swift:

  • To declare a subclass, put a colon after the class name and then add the name of the base class. What comes after the colon is called inheritance list. Note that Swift does not support multiple inheritance. Instead, the inheritance list can contain one base class name and one or more protocol names, which the class conforms to. More on protocols at the end of this chapter.

  • To override a method in a subclass, use the override keyword.

  • To access a parent entity’s method, property or subscript, use the super keyword, followed by a dot and the name of the member.

  • A subclass can access properties of superclasses in the hierarchy but has no way of knowing which properties are stored and which ones are computed. As a consequence, you can define property observers for any base class property in a subclass. The only condition is that the property you need to observe is accessible for modification (i.e., it’s not a stored constant or a computed read-only property).

  • In a subclass you can define custom getters and setters for properties of a base class either by overriding existing ones or by writing them from scratch for a stored property. You can even provide a setter for a read-only base class property and thus make it read-write. You can’t do the opposite and make a read-write property read-only, though.

  • Overriding a property setter and providing a property observer for a base class property are mutually exclusive: you can do one or the other, but not both at the same time.

  • You can prevent members of a class from being overridden in descendant classes by marking them with the final keyword.

  • You can use the final keyword to stop a whole class from being inherited.

Let us illustrate how subclassing works with an example. Listing 21-17 redefines the File class we have been working with throughout this chapter and defines another class, which inherits from it: TextClass. TextClass adds custom observers for the name property of the File class. It also overrides File‘s default initializer to customize the value of name.

Listing 21-17. Inheriting a Base Class
// File is a base class:
class File {
    var name: String


    init() {
        name = "unnamed file"
    }
}


// TextFile is a subclass of File:
class TextFile: File {
    // Declare property observers
    // for a base class property:
    override var name: String {
        willSet {
            print("About to change the file name to (newValue)")
        }


        didSet {
            print("The file name was changed from (oldValue) to
                   (super.name)")
        }
    }


    // Override the default initializer:
    override init() {
        // Call the superclass initializer:
        super.init()


        // Override the
        name = String("(name).txt")
    }
}

Extensions

Instead of inheriting an existing type in order to add functionality to it, you can extend it and add properties or methods to it after it has been defined. This applies to types you have written, as well as to SDK or other third-party types, whose implementation source code you can’t access.

To provide an extension for an existing entity, you use the extension keyword, followed by the name of the entity, followed by curly brackets in which you add the extending functionality. There are two things out of bounds to extensions:

  • You can only provide new functionality and can’t override existing methods, properties, or subscripts. This is in line with the open-closed principle we saw earlier.

  • You can add stored type properties but not stored instance properties. Anything you add to an extension is available to all instances of it, even if they were defined before the extension. Adding a stored property would change the memory footprint of an instance, which in Swift can’t be done at runtime.

We will see how this works by writing an extension to Swift’s String type (see Listing 21-18). To get the reverse of a String instance in Swift you need to call the reverse method on its characters property, which returns a new copy of the string’s character view. We will provide a method as a shortcut for that, which makes it look as if the string’s contents were reversed in place.

Listing 21-18. Adding a Method to the String Type
extension String {
    public mutating func reverse() {
        self = String(self.characters.reverse())
    }
}

A quick note: see the mutating keyword and the assignment to self inside the method we added? These aren’t relevant to providing an extension, but they are an important parts of Swift: we will see why later when we talk about structures.

You can now call the reverse method on an instance of String, as if the method was always part of the type (see Listing 21-19).

Listing 21-19. Calling an Extension Method
var palindrome = "Rats at a bar grab at a star."
print("Palindrome: (palindrome)")
palindrome.reverse()
print("Reversed palindrome: (palindrome)")


// output:
// Palindrome: Rats at a bar grab at a star.
// Reversed palindrome: .rats a ta barg rab a ta staR

See Chapter 22 for another example of extending the String type by adding a subscript to it, which lets us access individual characters in a string using a numeric index (e.g., myStr[2]).

Structures

Structures are a concept that doesn’t have a direct parallel in ActionScript, so let us introduce them by comparing and contrasting them with classes.

Like classes, structures keep together data in the form of properties and provide functionality in the form of methods and subscripts. You access a structure’s properties, methods, and subscripts using the same dot syntax you use with classes. A structure can also conform to protocols, which translated to ActionScript roughly means “to implement interfaces.” The syntax for declaring a structure looks very much like declaring a class, except that you use the struct keyword, instead of the class keyword.

Now, onto the differences between classes and structures .

  • Structures have a completely different way of managing memory —more on this below.

  • There is no subclassing: structures can’t inherit classes or other structures.

  • Structures don’t have deinitializer methods.

  • Structure methods are not allowed to make changes to the structure, unless you mark them as mutating.

  • You get an additional initializer automatically generated for you if you don’t provide initializers explicitly and ensure all stored properties of the structure have initial values. Read on to find out what these automatically generated initalizers look like.

The example in Listing 21-20 shows a structure definition, which holds the x and y coordinate of a point and has a Boolean method, which checks whether the point is at the origin of the coordinate system.

Listing 21-20. Defining a Structure
struct Position2D {
    var x: Int = 0
    var y: Int = 0


    func isAtOrigin() -> Bool {
        return 0 == x && 0 == y
    }
}

Instantiating a Structure

The rules for instantiating classes and defining initializers apply to structures too. For classes that provide default values for all of their stored properties, the compiler defines a default initializer without parameters behind the scenes. Structures get the same treatment and more: they also get a member-wise initializer automatically, which takes parameter values for every stored property.

This means that without adding any more code to the Position2D structure from the last example, we can create instances of it using one of the two automatically generated initializers, as shown in Listing 21-21.

Listing 21-21. Instantiating a Structure
// Uses the default initializer:
let origin = Position2D()


// Uses the memberwise initializer:
let randomPoint = Position2D(x: 10, y: 10)

Memory Management for Structures and Enumerations

Unlike classes, which use references, structures and enumerations are value types. In other words, when you instantiate a structure and assign it to a variable and then assign this variable to another variable and try to modify it, the second variable gets a copy of the structure and you end up with two structure instances. This is known as copy on write. The same thing happens when you pass a structure or an enumeration as an argument to a function: the value of the structure or enumeration gets copied over if there is an attempt to write to it.

Here is an example to illustrate that: we will try the same experiment we did with classes in Listing 21-12 to see the difference. The example in Listing 21-22 declares a variable pointA and initializes it with an instance of the Position2D structure we defined in Listing 21-20. It then declares a second variable, pointB, and assigns pointA to it. Now, if we change one of the properties of Position2D via pointB, we will see that the changes do not appear in pointA: pointA and pointB each has separate instances of Position2D.

Listing 21-22. Structures as Value Types
// pointA contains an instance of Position2D:
var pointA = Position2D(x: 5, y: 10)


// pointB gets a copy of this instance:
var pointB = pointA


// If pointB changes, this doesn't affect pointA:
pointB.x = 0
print(pointA.x) // prints out 5
print(pointB.x) // prints out 0

It’s worth being aware that most of the primitive types in Swift are implemented as structures behind the scenes: this includes all numeric types, the String type, and also the collection types Array and Dictionary.

Working with value types on the one hand means that you need to be careful when passing them around or doing assignments and to be aware of both the memory and performance overhead this might have. Having copies created every time there is an assignment takes additional memory and also takes time for initialization. On the other hand, Apple’s manual points out that things are optimized behind the scenes, so that actual value copying only takes place “when it is absolutely necessary” and that a programmer doesn’t necessarily need to go out of her way to avoid assignment of value types.

Mutating Methods

One of the rules for working with value types is that you cannot modify their properties from within instance methods. In order to enable a method to change the properties of a structure or an enumeration, you need to mark it as mutating.

Listing 21-23 adds two mutating methods to the Position2D structure: one of them changes the x and y properties; the second changes the whole structure by making an assignment to its implied self property.

Listing 21-23. Changing the Properties of a Structure Instance from Within a Mutating Method
struct Position2D {
    var x: Int = 0
    var y: Int = 0


    func isAtOrigin() -> Bool {
        return 0 == x && 0 == y
    }


    mutating func moveTo(x x: Int, y y: Int) {
        self.x = x
        self.y = y
    }


    mutating func moveTo(position newPosition: Position2D {
        self = newPosition
    }
}

When to Prefer Structures

Structures don’t use inheritance and are always passed by value. In other words, you can’t take advantage of a structure hierarchy to create sophisticated logic and you need to be careful with the memory footprint of your structures and the price you pay when new instances are automatically created. This makes structures best suited for encapsulating and keeping pieces of data together: the fewer, the better. Use classes when you require more complicated logic and need to take advantage of inheritance.

Enumerations

An enumeration or enumis a type that you declare to define a set of names. Like classes and structures, enumerations can have properties and methods and conform to protocols. Like structures, they are value types. Chapter 22 spends time delving into the subtleties of enumerations.

Protocols

A protocoldefines how an entity should be interacted with: what properties and methods other entities can use to access it. We will start by comparing and contrasting protocols with their ActionScript analogue: interfaces. Then we will depart as far away from ActionScript as we can.

Defining a Protocol

You define a protocol with the protocol keyword, followed by curly brackets. Inside the curly brackets is where the definition of the protocol’s requirement goes. This is much like defining a class or a structure, except that method, subscript, and property definitions have a slightly different syntax in a protocol.

Following is a protocol we will define as an example. It is called Countable and offers a way for a type, which contains a collection, to report the number of items in the collection and the unit for that number (Listing 21-24).

Listing 21-24. Defining a Protocol
protocol Countable {
    static var unit: String { get }
    func count() -> Int
}

Let us look at the requirements set by Countable.

The first one is that any conforming type should have a readable type property, called unit, which is of type String. The fact that the property is readable is expressed by the get keyword inside curly brackets after the property definition. Note that this requirement can be satisfied by a stored property, as well as a computed property with a getter. For a property that’s required to be readable and writeable, the definition would contain { get set }.

The second requirement defined by the protocol is that conforming types should implement a method, called count, which returns an integer. The protocol defines only the signature of the method, without an implementation, hence the lack of curly brackets.

Conforming to a Protocol

Now let us see how a type might conform to this protocol. We will redefine the Directory class that we used earlier in the chapter for that purpose (see Listing 21-25).

Listing 21-25. Conforming to a Protocol
class Directory: Countable {
    // Property, required by the Countable protocol:
    static var unit = "Files and folders"


    var files: [File] = []
    var directories: [Directory] = []


    // Method, required by the Countable protocol:
    func count() -> Int {
        return files.count + directories.count
    }
}

You can see that the protocol name appears in the declaration the same way you would put the name of a base class: after the type name, separated by a colon. The rule is: a type can conform to multiple protocols, which you list, separated by commas. If there is a base class the type inherits apart from the protocols, it should appear first in the list.

The definition of the property and the method, required by the Countable protocol, comes inside the definition of the Directory class like any other property or method. In this case an instance of Directory will return the number of files and directories it contains.

Facts About Protocols

We could easily dedicate a whole chapter to the power of protocols. However, for the scope of this book it is more important that you are aware of the things protocols can do for you and explore them in depth when you need to. Apple’s The Swift Programming Language online manual is a very good source.

  • Protocols can be adopted by classes, structures, and enumerations.

  • A protocol can set a restriction that it is only adoptable by classes by putting the keyword class in its inheritance list, for example:

    protocol Countable: class /*Any inherited protocols come after 'class'.*/ {}
  • Protocol methods that may need to change the internals of a type instance must be marked as mutating, in case the protocol is adopted by a structure or an enumeration.

  • Protocols can inherit each other and form hierarchies.

  • They can set requirements for initializers. When such an initializer is implemented in a conforming class, it is marked with the required modifier, unless the class has been defined as final. The required modifier signals to any subclasses that may inherit the conforming class that they need to provide an implementation for the initializer in order to conform to the protocol. This is illustrated with the example in Listing 21-26. It declares a protocol, named MyProtocol and a class that conforms to it, called MyBaseClass. The protocol defines an initializer (a default initializer without parameters in this case), which any conforming class needs to implement. When we define this initializer in MyBaseClass, we need to include the required keyword: this makes the initializer compulsory in MyDerivedClass, which we have declared as a subclass of MyBaseClass.

Listing 21-26. Making a Type Conform to a Protocol Through an Extension
protocol MyProtocol {
    init()
}


class MyBaseClass: MyProtocol {
    // The keyword required makes sure
    // that any subclasses of MyBaseClass
    // will also implement this initializer:
    required init() {
        // The body of the initializer
    }
}


class MyDerivedClass: MyBaseClass {
    // MyDerivedClass needs to implement this initializer
    // and include the keyword required, in case it has subclasses:
    required init() {
        super.init()
    }
}
  • You can define extensions to protocols like you do for other types.

  • You can make a type conform to a protocol via an extension. As an example let’s play with Swift’s String type again to make it conform to the Countable protocol we defined earlier and have it report the number of characters it contains (Listing 21-27).

Listing 21-27. Making a Type Conform to a Protocol Through an Extension
extension String: Countable {
    static var unit: String {
        get {
            return String(Character.self)
        }
    }


    func count() -> Int {
        return self.characters.count
    }
}

You will see that String implements the required unit property differently. Instead of a stored property, unit is defined as a calculated property and returns the type name of Character as a String.

Let’s test the extension by calling count on a String instance, as shown in Listing 21-28.

Listing 21-28. Testing Protocol Conformance
let s = "Ready, fire, aim!"
print("'(s)' has (s.count()) instances of (String.unit).")


// output:
// 'Ready, fire, aim!' has 17 instances of Character.

The Merit of Protocols

Protocols can be used as types in their own right. For example, if you declare a function that takes a parameter of type Countable, you can then call that function and pass it a String instance or a Directory instance. This way protocols let structures and enumerations take advantage of polymorphism, even if they can’t use inheritance.

The main advantage of protocols, however, will be revealed when we look at generics in Chapter 22 . Generic programming allows you to write code that can be applied to many different types of data. Protocols help add constraints to that and make generic types really powerful.

Summary

In this chapter we covered the tools and techniques that Swift provides for Object-Oriented Programing. In particular we saw the differences between classes and structures, the variety of ways that Swift can handle properties, and how we can extend types without modifying them. Finally we covered the basis of protocols, the power of which we will see in the next chapter, as we cover Swift’s support for generics. Stay tuned . . .

Footnotes

1 . . . also mildly irritating—but mostly wonderful, trust me.

2 Extensions are not a new concept that appeared with Swift. You will find the same idea in Objective-C, where it is called categories.

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

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