Constructing Classes, Structures, and Enumerations
As alluded to in previous chapters, and revealed fully in this one, Swift breaks new ground with class types, and upgrades structures and enumerations to first-class status. In fact, there is less to say about the differences than similarities between classes and structures in Swift, and enumerations are not far off the mark, either. This chapter will introduce each of these constructs, first at a high level and then followed by deeper analysis of major considerations such as initialization, definition of properties and methods, and selection guidelines.
Naming Conventions
Swift follows the same naming conventions as Objective-C with regard to naming classes, structures, enumerations, properties, and methods. Classes, structures, and enumerations are all formal types, and thus their names should begin with a capital letter and use camel casing. Properties and methods are not types, so, in order to differentiate them from types, their names should begin with a lowercase letter and also use camel casing.
Classes and Structures
Objective-C classes are the workhorses of the language. From defining properties and methods to declaring protocols and calling on delegates, classes can satisfy a wide variety of needs. Objective-C is a superset of C and utilizes basic C structures as an alternative to classes. C structures are lighter-weight, although they are limited to storing scalar values (i.e., they cannot hold references to objects), and they cannot define methods.
Swift balances the workload between classes and structures, and as mentioned in Chapter 2, all of Swift’s basic data types are implemented as structures (except Character, which is implemented as an enumeration). Table 7-1 compares classes and structures in Objective-C and Swift. Attributes are aligned horizontally to aid in cross-referencing.
Table 7-1. Comparison of classes and structures in Objective-C and Swift
Objective-C |
Swift | |
---|---|---|
Class |
Defines initializers Can define properties Can define instance variables Can define static variables Can implement custom deallocation logic |
Defines initializers Can define stored instance properties Can define a deinitializer |
Can use lazy instantiation of properties Can define instance methods Can define class methods Almost always subclasses Can be subclassed Can have extensions and categories |
Can define lazy stored properties Can define instance methods Can define type methods Can subclass Can be subclassed Can have extensions | |
Can be type checked and casted Can define computed-property-like instance methods Can define computed-property-like class methods Can override property accessors |
Can conform to protocols Can be type checked and casted Can define computed instance properties Can define computed type properties Can define property observers Can define subscripts | |
Can have multiple references Passed by reference |
Can have multiple references Passed by reference | |
Structure |
Defines members |
Defines stored instance properties |
Can define initializing functions Passed by copy |
Has automatic memberwise initializers Passed by copy | |
Can define stored type properties Can define computed instance properties Can define computed type properties Can define instance methods Can define type methods Can define subscripts Can have extensions Can conform to protocols |
Although there seems to be general feature parity between Objective-C and Swift classes, it can be quickly deduced just by glancing over Table 7-1 that structures in Swift have many more capabilities. All of these similarities, differences, and new capabilities will be covered in the forthcoming sections.
Classes
There are three standout differences between classes in Objective-C and Swift:
This chapter will cover the first two of these variances. Chapter 9 will deal with subclassing-specific topics, and Chapter 10 will analyze access control.
Objective-C’s use of separate interface and implementation files has evolved over time, to the extent that many in the community have questioned the continued necessity of even having separate files. Swift answered that question: no, it’s not necessary. Whereas class source files in Objective-C have a file extension of either .h or .m—for declaration and implementation code, respectively—all production source code files in Swift reside in a .swift file. In the case of Swift playgrounds, a .playground file is used. Deferring the broader topic of access control for now, it is sufficient to understand here that the external interface for each Swift class (or structure) is made available to all other code within the module. Swift recognizes each build target in an Xcode project as a separate module, and modules can be imported. Table 7-2 compares the basic syntax of a class definition in Objective-C and Swift, absent of details for properties and methods, which will be covered later in this chapter. Optional components are italicized, and in order to keep these comparisons straightforward, accoutrements such as constants, macros, and extensions in Objective-C are omitted.
Table 7-2. Basic syntax of class definitions in Objective-C and Swift
Objective-C |
// In .h interface file importStatements @interface ClassName : ParentClassName <ProtocolName, ...> publicPropertyDeclarations publicMethodDeclarations @end // In .m implementation file importStatements @implementation ClassName <ProtocolName, ...> { instanceVariables } privatePropertyDeclarations methodImplementations @end |
Swift |
// In .swift file importStatements class ClassName: ParentClassName, ProtocolName, ... { propertyDefinitions methodDefinitions } |
Notice in the Swift example that defining a parent class is optional. Although it is technically also optional in Objective-C, most every class in Objective-C is a subclass of NSObject, for at least one reason: to inherit the +[NSObject alloc] method. Without that method, a class would have to implement its own memory allocation process and return an instance of the class to then be initialized. Classes in Swift are self-efficient with regard to the whole instantiation, initialization, and deinitialization process, inclusive of memory allocation. Also notice that a class in Swift may adopt one or more protocols, and protocols should be listed after the parent class (if there is one).
Structures
C structures in Objective-C facilitate storing scalar values, aggregates, or other structures as its members, and are passed by copy. Member values are normally retrieved and set using dot notation syntax, and although they can also be instantiated and referenced via structure pointers, this is less common in Objective-C programming where a class is typically used for anything but simple pass-by-copy data structures.
Table 7-3 compares the basic syntax of structure definitions in Objective-C and Swift; properties (and methods in Swift, which are optional) will be covered shortly.
Table 7-3. Basic syntax of structure definitions in Objective-C and Swift
Objective-C |
typedef struct { memberDeclarations } StructName; |
Swift |
struct StructName { propertyDefinitions methodDefinitions } |
Structures in Swift have been elevated to be nearly as capable as classes, with a few distinguishing differences that also help with determining whether to choose a class or structure for a particular need (selection guidelines are discussed later in this chapter in the section Selection Guidelines). The most notable differences between Swift structures and classes are:
Enumerations
Enumerations represent another chasm between Objective-C and Swift. Enumerations enable grouping related values together as a specific type of value. An enumeration can be iterated over (such as in a switch statement), and enumeration members can be passed as parameters to a function. Additionally, member names receive the benefit of code-completion, thus eliminating typo-related bugs that can be common when using raw strings. Enumerations are also commonly used with bitwise shift operators to combine multiple members into a single bitmask value, for example, for use in setting options in a method.
Objective-C utilizes straight C enumerations, adding helpful macros such as NS_ENUM and NS_OPTIONS that aid in defining new typedef’d enumerations. Members are named and represent differing, usually incremental, integer values (bitmasks in the case of NS_OPTIONS). Swift takes enumerations to a whole new level. One of the biggest differences is that Swift enumerations do not automatically assign a default integer value; member values are, by default, fully-fledged values of the enumeration type. Table 7-4 compares enumerations between Objective-C and Swift. Attributes are aligned horizontally to aid in cross-referencing.
Table 7-4. Comparison of enumerations in Objective-C and Swift
Objective-C |
Swift |
---|---|
Defines members via comma-separated list |
Defines members via case statements Can define multiple members in single case statement (comma-separated list) |
Members defined with default integer value |
Members are not automatically assigned a raw value Member default values are fully-fledged values of the enumeration type Member raw values can be a string, character, integer, or floating-point number |
Member values auto-increment if not explicitly assigned an integer value |
If a raw integer value is assigned, subsequent member values auto-increment if not explicitly assigned an integer value Can define associated member values of any type and/or multiple types Can define initializers Can define computed instance properties Can define computed type properties Can define stored type properties Can define instance methods Can define type methods Can define subscripts Can have extensions Can conform to protocols Passed by copy |
Table 7-5 compares the basic syntax of an enumeration definition in Objective-C and Swift; optional components are italicized.
Table 7-5. Basic syntax of enumeration definitions in Objective-C and Swift
Objective-C |
typedef NS_ENUM(NSInteger, EnumName) { |
Swift |
enum Enum1Name { |
Two versions of a Swift enumeration are included in Table 7-5, as individual enum members in Swift can be specified in individual case statements or in a single, comma-separated listed in one case statement.
Initialization
As already noted, Objective-C uses C-based structures and enumerations that do not have formal initializers (although initializer functions or factory methods could be created to vend initialized instances). Objective-C classes, along with Swift classes, structures, and enumerations, all define or inherit initializers that prepare an instance for use. The process of instantiation—which includes allocating the memory for an instance and then initializing it—is similar in outcome but significantly different in implementation between the two languages.
Class Initialization
Objective-C classes can define one or more initializers, typically with one designated initializer, that can optionally set default property values and perform other initialization tasks. Instantiating an Objective-C class instance involves first calling +[<NSObject subclass> alloc] to obtain an instance of the receiving class (memory allocated), and then passing that instance to an -[<NSObject subclass> init] method to complete initialization, or by calling a convenience class method such as +[<NSObject subclass> new] (which in turn simply calls [[<NSObject subclass> alloc] init]), or by calling any number of additional convenience constructors that may be available to receive a fully set up object back from a single method call that abstracts the alloc/init calls. This is an oversimplification of an elaborate process that has evolved over many years (Automatic Reference Counting, a.k.a. ARC, is one of the most profound improvements). Yet a comparison can now be made with how Swift handles class instantiation. Swift, also using ARC, handles all memory-management responsibilities associated with instantiating a class instance.
Swift classes must ensure that all nonoptional property values are set on initialization, and this is carried out by either setting default values in property declarations and/or in an initializer. Unlike Objective-C initializers, which return an initialized instance of the class, a Swift initializer’s sole responsibility is to ensure that a new instance is properly set up before use.
Tip A general best practice is to prefer setting a property’s initial value in its declaration versus setting it in an initializer, whenever possible. This makes for more concise code, streamlines initializers, and benefits initializer inheritance (Chapter 9 will cover initializer inheritance in detail).
For base classes (i.e., that do not inherit from another class) wherein all nonoptional properties are assigned a default value during declaration, Swift automatically provides a default initializer that will set all the properties to their default values.
Initialization in Objective-C is typically carried out by assigning the response of a call to –[super init] (or a variation) to self, followed by (optionally) assigning initial values directly to the backing instance variables, and performing any other initialization tasks.
Swift defines two kinds of initializers: designated and convenience. Designated initializers are primarily responsible for ensuring that all of the class’ properties are initialized. Every class must have at least one designated initializer, which can be inherited from a superclass (i.e., if the class has a superclass). Additionally, in the case in which there is a parent class, a designated initializer must also call a designated initializer in the immediate superclass; see Chapter 9 for details. Convenience intitializers are just that, initializers that conveniently abstract some of the work associated with initializing an instance of the class. They are optional, and marked with a convenience modifier before the init keyword in the function definition. A convenience initializer may call another convenience initializer or a designated initializer, for example, self.init(parameterName: parameterValue). However, all convenience initializers must eventually point to a designated initializer. Apple’s Swift language guide summarizes how designated and convenience initializers should be chained together, “Designated initializers must always delegate up. Convenience initializers must always delegate across.”
Initialization in Swift is a two-phase process that is similar to initialization in Objective-C, except that Swift allows setting custom initial values in phase 1 versus in Objective-C, every property is initially assigned 0 or nil. In phase 1, memory is allocated, all nonoptional properties of the class are assigned an initial value, and the superclass (if one exists) is given the opportunity to assign values to all its nonoptional values (this repeats all the way up the inheritance chain). Essentially, phase 1 establishes self, which can then be accessed in phase 2 in order to further customize the instance. Table 7-6 provides examples of a basic class being defined and instantiated in Objective-C and Swift.
Table 7-6. Comparing definition and instantiation of a class in Objective-C and Swift
Objective-C |
// In MyCustomClass.h |
Swift |
// In .swift file |
Notice in the Swift example in Table 7-6 that the property title is of type String!. Remember from Chapter 2 that an ! can be used to implicitly unwrap an optional during declaration. In this example, the instances’ title property is set during instantiation, however it can be subsequently set to nil, because it is an optional value. Observe also that the init() method automatically included the external title parameter name, even though it was not explicitly stated. As pointed out in the last chapter, Swift methods (functions defined within a type), automatically provide an external name for the second and subsequent parameters of a method. However, for init() methods, an external name is automatically provided for all parameters, including the first one.
Tip To prevent an external name from automatically being created for an initializer parameter, write an underscore (_) before the parameter name, where an explicit external parameter name would normally be stated.
The use of self in the init() method was necessary to disambiguate the parameter from the property. Had the parameter been named aTitle, for example, the line setting the title property could have omitted using self, that is, title = aTitle. Also note that a computed type property was used, because, as of this writing, Swift does not support stored class properties; attempting to create one generates the compiler error: “class variables not yet supported.”
Structure Initialization
As in Swift classes, Swift structure initializers must ensure that all nonoptional properties are set to an initial value, unless a property is set to a default value in its declaration.
When all nonoptional properties are assigned a default value in their declaration, a default initializer is automatically provided to set those initial values. Yet even when a structure does not set all of its nonoptional properties, if no custom initializer is defined, a memberwise initializer is automatically created, which essentially results in each property receiving an external name. That said, if a custom initializer is defined, neither a default initializer nor memberwise initializer will be available.
Tip To regain access to the default initializer for a structure that declares one or more custom initializers but also sets default values for all nonoptional properties, simply define an empty initializer:
init() {}
Table 7-7 demonstrates the definition, instantiation, and usage of a structure in Objective-C and Swift.
Table 7-7. Comparing definition, instantiation, and usage of a structure in Objective-C and Swift
Objective-C |
typedef struct { |
Swift |
struct MyStruct { |
Enumerations are comparatively simple to define and instantiate in both Objective-C and Swift, as Table 7-8 demonstrates.
Table 7-8. Comparing definition and instantiation of an enumeration in Objective-C and Swift
Objective-C |
typedef NS_ENUM(NSInteger, MyEnum) { |
Swift |
enum MyEnum1 { |
In the Swift examples, notice that MyEnum1 does not define a type, referred to as the raw value type for enumerations, whereas MyEnum2 defines that it has a raw value of type Int. As pointed out in Table 7-4, MyEnum1 values are of type MyEnum1. Swift enumeration member raw values must be literal, which means it is not possible to use bitwise shifting to assign values (as in the NS_OPTIONS example). The raw value of a Swift enumeration that defines a raw value can be accessed via the read-only rawValue property. Also notice that enumeration values can be assigned using short dot syntax (instead of explicitly stating the type, e.g., MyEnum.value1), because the type can already be inferred.
Under certain circumstances, such as when a required external resource is not available during initialization or an invalid parameter value is passed to an initializer, initialization may fail. One or more failable initializers can be defined for a class, structure, or enumeration, in order to handle the possibility that initialization may not succeed. As previously mentioned, a Swift initializer does not return an instance of the type but, rather, it ensures that all nonoptional property values are set. A failable initializer, however, will either succeed and result in the creation of a fully-initialized optional value of the type, or it will fail and must explicitly return nil.
To define an initializer as failable, write a ? immediately after the init keyword. As with other uses of optional values, an instance initialized with a failable initializer must be unwrapped before using. That said, a failable initializer may also be defined such that it will result in the creation of an implicitly unwrapped optional instance. To define an implicitly unwrapped failable initializer, write an ! immediately after the method name.
Note Failable or not, an initializer cannot be defined with the same parameter names and types as another existing initializer for the same type.
The Swift standard library automatically defines the failable initializer init?(rawValue:) for enumerations defined as having a raw value. Table 7-9 provides examples of failable initializers for classes, structures, and enumerations.
Table 7-9. Examples of class, structure, and enumeration failable initializers in Swift
Class |
class Person { |
Structure |
struct Polygon { |
Enumeration |
import Foundation |
Both Objective-C and Swift classes can store values as properties, yet Swift structures and enumerations can also have properties. Table 7-10 itemizes the kinds of properties each Swift construct can define.
Table 7-10. Itemizing available property types for classes, structures, and enumerations in Swift
Stored type properties are marked with the static keyword in the definition. Computed type properties are marked with the class keyword for classes, and static keyword for structures and enumerations.
Properties in Objective-C are typically defined with explicit memory retention and atomicity rules, whereas in Swift, all value types (i.e., structures and enumerations) are copy, and all reference types (e.g., classes) are strong. There is one exception to this rule, which will be covered in the section Declaration and Type Attributes. Of atomicity, Apple states to use, “...dispatch_once to make sure that the initialization is atomic.,” (https://developer.apple.com/swift/blog/?id=7). An example of when this exception would be necessary will be demonstrated in the section Singletons.
Swift properties are regular stored or computed values of a class, structure, or enumeration, declared as variables or constants within the class, structure, or enumeration. Although Objective-C properties are automatically synthesized with backing instance variables, no such construct exists in Swift. The backing store of a Swift property is not directly accessible, and access to a property’s value is facilitated via the getter and setter of that property. A Swift property with only a getter is read-only, just like an Objective-C property with the readonly property attribute.
Stored type properties in Swift can be set (if declared as a variable) and retrieved on a structure or enumeration type itself, just like instance properties. This is similar to static variables (mutable) and constants (immutable) in Objective-C, except that static variables and constants are accessed directly instead of as properties of the class using dot notation, as they are in Swift.
Note Stored type properties must always be given an initial value, since there is no initializer to set them.
A property can be lazily instantiated in Objective-C by overriding the getter and working directly with the backing instance variable for the property. Swift has lazy stored properties (variable stored properties marked with the lazy modifier) that achieve the same goal of delaying calculation of a property’s initial value until the first time it is retrieved.
Note Constant properties in Swift must be set during declaration or in an initializer and cannot be marked as lazy.
Swift computed properties can be retrieved and set (if variable) like a regular stored property, but return a computed value or process a set value (such as to indirectly set other properties), respectively. A computed property can optionally have only a getter and setter, and when it has only a getter, the get keyword can be omitted. A computed setter can specify a parameter name for the new value, enclosed in parentheses after the set keyword. If no parameter name is specified and the parentheses are omitted, a default newValue parameter name is automatically created and made available for use.
A Swift property can be initialized using a closure, which is similar to using a block within a custom getter in Objective-C. Keep in mind that, during initialization, the instance has not yet been fully initialized, so other properties should not be accessed in the closure, nor should self be accessed or any other methods called. In order to execute the closure immediately, write () immediately following the closing curly brace of the closure.
Changes to a Swift property can also be observed via a property observer. Property observers respond to changes in a property’s value by calling the willSet and didSet and passing along the new or old value, accordingly. Similar to computed properties, if a parameter name is not specified in parentheses following the implementation of a willSet or didSet method, a parameter name of newValue or oldValue, respectively, is automatically created.
Note willSet and didSet are not called during initialization, nor are they called if the property is changed within either method. However, they are called even if the property is set to a new value that is equal to the current value.
In Objective-C, public methods can be used to achieve an effect similar to Swift computed properties, and public property accessors can be overridden to provide Swift’s property-observer-like handling of changes to a property’s value.
Table 7-11 provides examples of each of the aforementioned property types in Objective-C and Swift. To keep the comparison clear-cut, the Objective-C computed instance example does not include type checking, which would be necessary for production code. An example of using a block or closure to initialize a property will be included in Chapter 9.
Table 7-11. Examples of property definitions in Objective-C and Swift
Objective-C |
Swift | ||
---|---|---|---|
Stored instance |
@property (copy, nonatomic) NSString *title; |
var title: String! | |
// In HttpStatusChecker.h |
struct HttpStatusChecker { |
||
static NSString *defaultTitle = @"A Worthy Title"; |
// In structure or enumeration |
||
Computed class/type |
// In DateChecker.h |
class DateChecker { | |
// In TitleTracker.h |
class TitleTracker { |
Enumeration Associated Values
Swift enumerations can also associate passed-in values to its members. Each enumeration case can be assigned the passed in value, and value types can be of any type and even different between cases. Associated values can also be extracted in a switch statement using value binding:
enum BookID {
case isbn10(Int)
case isbn13(String)
case oclc(String)
case lccn(String)
case olid(String)
var title: String! {
var t: String!
switch self {
// Simulating setting t via API call or query
case let .isbn10(id):
t = "Transitioning to Swift"
case let .isbn13(id):
t = "Transitioning to Swift"
case let .oclc(id):
t = "Transitioning to Swift"
case let .lccn(id):
t = "Transitioning to Swift"
case let .olid(id):
t = "Transitioning to Swift"
}
return t
}
}
let bookId = BookID.isbn10(1484204077)
println(bookId.title) // Prints "Transitioning to Swift"
Objective-C classes can add array- or dictionary-like subscripting by overriding –objectAtIndexedSubscript: and/or -setObject:forKeyedSubscript:, taking a key (and object) to get (or set) an object, respectively. In Swift, classes, structures, and enumerations can all define subscripts that can take multiple input parameters and/or a variadic parameter, be multidimensional (same as collection types), be read-write or read-only (using getters and setters, just like computed properties), return any value type (including multiple values in a tuple), and can even be overloaded. They cannot, however, use inout parameters to modify pass-by-reference parameters, nor can they define default parameter values:
class Introspector {
var values: Any!
subscript(theValues: Int...) -> (sum: Int, average: Double) {
values = theValues
var sum = 0
for integer in theValues {
sum += integer
}
let average = Double(sum) / Double(theValues.count)
return (sum: sum, average: average)
}
subscript(theValues: Double...) -> (sum: Double, average: Double) {
values = theValues
var sum = 0.0
for value in theValues {
sum += Double(value)
}
let average = sum / Double(theValues.count)
return (sum: sum, average: average)
}
}
let myIntrospector = Introspector()
let result1 = myIntrospector[1, 2, 3, 4, 5]
println("For (myIntrospector.values), sum is (result1.sum) and average is (result1.average).") // Prints "For [1, 2, 3, 4, 5], sum is 15 and average is 3.0."
let result2 = myIntrospector[1.1, 2.2, 3.3, 4.4, 5.5]
println("For (myIntrospector.values), sum is (result2.sum) and average is (result2.average).") // Prints "For [1.1, 2.2, 3.3, 4.4, 5.5], sum is 16.5 and average is 3.3."
Methods
In Objective-C, only classes can define methods, which can either be instance or class methods. In Swift, classes, structures, and enumeration can all define methods, including both instance and type methods. Swift methods are simply functions that are associated to a specific type. For example, initializers are instance methods (in both Objective-C and Swift). Remember from the last chapter that parameters of functions (and thus methods) are constants by default; explicitly declare a parameter as a variable by using the var prefix. Objective-C methods are called on instances and classes using bracket notation. Swift methods use the same dot syntax to call methods as to access properties.
Write the keyword static before the method name for structure and enumeration type methods, and write the class keyword for class type methods. As mentioned previously, Swift methods automatically provide an external name for the second and subsequent parameters when calling the method. Figure 7-1 compares the same function as a global function and as a method within a class.
Figure 7-1. Comparing a global function to method in Swift
Presumably, the intention of this additional feature of methods over regular functions is to maintain the style of Objective-C method inline parameter names and general self-documenting readability. Although the examples in Figure 7-1 demonstrate this difference, good form would actually be to provide an explicit external name for the second parameter, as demonstrated in Table 7-13.
When calling an instance method within another instance method within that same type, it is not necessary to explicitly state self. The same goes for calling a type method within another type method of the same type:
struct Incrementor {
static var value = 0
static func increment(_ increment: Int = 1) {
value++
println(value)
}
init() {
printCurrentValue()
}
func printCurrentValue() {
println("Current value: (Incrementor.value)")
}
func topLevelFunc() {
func nestedFunc() {
printCurrentValue()
}
nestedFunc()
}
}
Incrementor.increment() // Prints "1"
Incrementor.increment() // Prints "2"
var typer = Incrementor() // Prints "Current value: 2"
typer.topLevelFunc() // Prints "Current value: 2"
Notice in this example that it is not necessary to specify Incrementor for value in increment(), nor is it necessary to specify self for printCurrentValue() in init(), or in the nested function nestedFunction(), yet it is necessary to specify Incrementor.value in printCurrentValue(), because that is an instance function accessing a type property.
By default, instance properties of value types cannot be changed from within their own instance methods. Because enumerations cannot have instance properties, this rule applies only to structures. To opt in to the mutating behavior of an instance method, such that it can change instance property values and write back those changes when the method ends, write the mutating keyword before the func keyword in the definition. A mutating method in Swift can even instantiate a completely new instance of that type and assign it to self, which is not possible in Objective-C. Class instance properties, as well as type properties of value types (i.e., structures and enumerations) can be mutated within the type’s instance methods.
Table 7-12 provides examples of Objective-C methods and Swift functions, specific to the unique features of methods over functions as described in this section; the Objective-C method calls are being made within –[SomeOtherClass someMethod] in SomeOtherClass.m. Refer to the last chapter for more detailed coverage of Swift functions in general.
Table 7-12. Comparing unique features of Swift methods over functions to Objective-C methods
Objective-C |
Swift | |
---|---|---|
// In SalutationPrinter.h |
class SalutationPrinter { | |
Instance method |
SalutationPrinter *salutationPrinter = [SalutationPrinter new]; |
let salutationPrinter = SalutationPrinter() |
Class/type method |
[SalutationPrinter print:@"Hello" toRecipient:@"Henry"]; // Prints "Hello Henry" |
SalutationPrinter.print("Hello", toRecipient: "Henry") // Prints "Hello Henry" |
Mutating instance method |
N/A |
var counter = Counter() |
Declaration and Type Attributes
Swift classes can utilize declaration attributes just like stored values and functions to provide additional information:
@availability(iOS, obsoleted=1.0, message="Use NewIOSClass instead")
class OldIOSClass { /* ... */ }
@availability(iOS, introduced=0.8)
class NewIOSClass { /* ... */ }
// Originally implemented in release 0.1
@availability(*, introduced=0.1)enum MyStruct { /* ... */ }
// Renamed in release 1.0
@availability(*, introduced=1.0, renamed="MyRenamedStruct") enum MyRenamedStruct { /* ... */ }
typealias MyStruct = MyRenamedStruct
In these examples, OldIOSClass is an iOS-only class that was obsoleted in 1.0, and its usage will be prevented by a compiler error; it was replaced by NewIOSClass in 0.8 (and, presumably, from 0.8 to 1.0, OldIOSClass was marked deprecated). MyStruct was renamed in 1.0 to MyRenamedStruct, and a typealias is used to ensure existing code will still work with the old name.
As previously stated, reference types properties in Swift have strong memory retention by default. However, reference type properties marked with the declaration attribute @NSCopying will have their setter synthesized with a copy of the passed in value, similar to how the copy property attribute works in Objective-C. That property’s type must also conform to the NSCopying protocol; protocols are covered in the next chapter.
Although the topic of mixing Objective-C and Swift source files in the same project is beyond the scope of this book, there is one declaration attribute use case specific to classes that should be mentioned here. The @objc attribute can be used to mark a class as available to Objective-C source code within the same module. An Xcode-generated header must also be imported within an Objective-C source code file in order to expose the Swift source code files to it. The generated header file has the name ProductModuleName-Swift.h, where ProductModuleName is the name of the target. When exposing a Swift class to Objective-C code, a different name can be provided for use by the Objective-C code by placing that name in parentheses immediately after the @objc attribute. And for classes marked with the @objc attribute, individual entities within that class can have different names exposed to Objective-C by marking those entities with the @objc attribute.
Note Swift classes that are intended to be made available to Objective-C code should subclass NSObject in order to inherit +[NSObject alloc].
// In SwiftClass.swift
import Foundation
@objc class MySwiftCustomClass: NSObject {
var enabled: Bool
init(enabled: Bool) {
self.enabled = enabled
super.init()
}
}
// In SomeOtherClass.m
#import "Transitioning-Swift.h"
@implementation SomeOtherClass
- (void)someMethod
{
MySwiftCustomClass *swiftClassInstance = [[MySwiftCustomClass alloc] initWithEnabled:NO];
}
Note Apple provides a useful guide for mixing Swift and Objective-C code in the same project, which includes listing Swift-only features that are not accessible within a class or protocol marked with the @objc attribute: Using Swift with Cocoa and Objective-C (http://bit.ly/mixswiftandobjc).
See Chapter 2 and the last chapter for a list of additional declaration attributes and examples. Swift classes, structures, and enumerations can use autoclosure and noreturn type attributes in their method declarations in the same manner as described in the last chapter for using type declarations with functions.
Deinitialization in Swift applies only to class types, and is similar to deallocation in Objective-C, except that deinitialization is what happens right before a Swift class get deallocated. Because of this, a deinitializer in Swift has full access to its instance, including all its properties. A class can have at most only a single deinitializer, which is defined as a deinit method that takes no parameters and, uniquely, is written without parentheses. deinit is automatically called right before deallocation and should not be called directly. Although the topic of subclasses is deferred until Chapter 9, it should be mentioned here that a subclass inherits its superclass’ deinitializer, and the superclass’ deinitializer is automatically called at the end of the subclass’ deinitialization, even if an explicit deinit method was not defined for the class. Swift automatically deallocates an instance when it is no longer referenced (via ARC), so explicitly defining deinit is not generally necessary unless some manual process or cleanup must be performed right before deallocation. Table 7-13 compares a common use case for –[NSObject dealloc] in Objective-C and deinit in Swift.
Table 7-13. Comparing use of –[NSObject dealloc] in Objective-C and deinit Swift
Objective-C |
// In KeyboardDisplayHandler.h |
Swift |
class KeyboardDisplayHandler { |
Avoiding Strong Reference Cycles
Because Swift classes are reference types, and thus a single class instance can have multiple (default strong) references to it, a situation can occur where two class instances can hold a strong reference to each other, such that neither will ever be deallocated. This is a strong reference cycle, a familiar situation to Objective-C developers. And the approach to avoid a strong reference cycle between classes in Swift is similar to the approach taken in Objective-C.
In Swift, a weak reference must be declared as optional variables, as their value must be able to be set to nil during runtime. By contrast, an unowned reference in Swift always will have a value and cannot be set to nil.
Tip If a class will always have a reference to another class, mark the property that points to an instance of that other class as unowned. Otherwise, if the class may not always have a reference to another class, mark the property as weak.
Table 7-14 provides comparative examples of avoiding strong reference cycles in Objective-C and Swift.
Tip Swift playgrounds do not consistently call deinit. As such, while the println() statements in the example code in Tables 7-14 and 7-15 would print out if run in an Xcode project, they may not print out when run in a playground.
Table 7-14. Avoiding strong reference cycles in Objective-C and Swift
Objective-C |
// In MyCustomClass.h |
Swift |
class Employee { |
In Objective-C, capturing self in a block will also result in a strong reference cycle, because blocks maintain a strong reference to any captured objects, including self. A similar situation can also occur in Swift that results in a strong reference cycle. Closures are reference types, just like classes. So, if a closure that is assigned to a property in a class instance calls self in any way, such as to access another property or call a method, a strong reference cycle will occur.
Note To help avoid creating a strong refence cycle, Swift requires explicitly stating self within a closure, such as self.someProperty or self.someMethod().
The solution in Objective-C is to create a weak reference to self ahead of and for use in the block.
Swift, by contrast, employs closure capture lists. A closure can define a capture list as part of its overall definition, to specify if one or more captures should be handled as weak or unowned references. As with nonclosure properties, a weak reference must always be declared as an optional variable.
Tip If the capture can be set to nil, such as if the instance it references gets deallocated, define the capture as weak. Otherwise, if a closure and a capture within that closure will always refer to each other and be deallocated at the same time, define the capture as unowned.
The syntax to define a capture list is to enclose a rule (or multiple rules in a comma-separated list) inside square brackets, within the closure body, after the opening curly brace and typically on the next line, before the closure’s parameter list and return value (if provided and cannot be inferred), followed by the in keyword. A rule consists of the weak or unowned keyword followed by a single capture. The basic syntax is as follows:
lazy var someClosure: (ParamType, ...) -> ReturnType = {
[weak self] (paramOneName: ParamType, ...) -> ReturnType in
statements
}
An arbitrary expression can also be bound to a named value in a capture list, as demonatrated in Table 7-15, which compares how to avoid strong reference cycles in Objective-C blocks and Swift closure properties of a class instance.
Table 7-15. Avoiding strong reference cycles in Objective-C blocks and Swift closure properties of a class instance
Objective-C |
// In MyCustomClass.h |
Swift |
class Bravo { |
Singletons
In the same blog post cited earlier referencing atomicity in Swift, Apple further states that the initializer for global variables and for static members of structures and enumerations is run on first access, using dispatch_once from GCD (Grand Central Dispatch being right up there with ARC as a major milestone addition to the platform) to ensure the initialization is atomic. With that said, Table 7-16 compares creating singletons in Objective-C and Swift (credit goes to Stack Overflow user “David” for the elegant Swift example he provided: http://stackoverflow.com/a/24073016/616764).
Table 7-16. Comparing creation of singletons in Objective-C and Swift
Objective-C |
// In StoreManager.h |
Swift |
class StoreManager { |
Eumerations offer many new capabilities in Swift, such that an enumeration could actually handle a need that would normally be served by a class or structure. For example, an enumeration could be used to capture state that has nothing to do with an actual sequence of related values. In the spirit of the old adage, “Just because you can does not necessarily mean you should,” an enumeration should really only be chosen when an enumeration is needed. The new capabilities afforded enumerations in Swift should be taken advantage of in conjunction with satisifying an enumerative need, not in lieu of choosing a more appropriate type when there is no such need.
The near-feature parity nature of classes and structures in Swift can make choosing one over the other more of a challenge. While there are certainly going to be use cases where it won’t really matter which type is used, Table 7-17 offers some basic guidelines to help discern when one really is a better choice over the other for a particular need.
Table 7-17. Guidelines for selecting a class or structure in Swift
Class |
Structure | |
---|---|---|
Needs to inherit certain capabilities, initially or perceivably later |
✓ |
|
Needs to be able to be inherited from |
✓ |
|
May need to be type casted |
✓ |
|
Needs to store type properties to be shared with all instances |
✓ | |
Needs broadest range of storage options (i.e., stored and computed instance and type properties) |
✓ | |
Needs to store simple values |
✓ | |
Needs to store large values |
✓ |
|
May need to perform some final process or do manual cleanup before being deallocated |
✓ |
|
Will only ever need to be copied when passed around in code (e.g., to a function) |
✓ | |
Should be referenced when passed (e.g., to a function) |
✓ |
|
Needs to be a singleton instance |
✓ |
|
Needs to (optionally) conform to optional protocols |
✓ |
All of these guidelines are based on topics covered thus far, except optional protocols, which will be introduced in Chapter 8.
Summary
In this chapter, the differences between Objective-C and Swift classes, structures, and enumerations—of which there are many—and the similarities between Swift classes, structures, and enumerations—of which there are also many—were examined. Copious examples and tables summarizing rules and features were provided to accompany narrative explanations in an effort to expose sometimes subtle distinctions. Additionally, selection guidelines were introduced to help choose the best tool for the task at hand.
18.225.55.151