Learning Swift — Days 114-117

Review of the Swift Programming Language

Day 114

  • Type Methods: we indicate type methods by writing the static keyword before the method’s func keyword. Classes can use the class keyword instead, to allow subclasses to override the superclass’s implementation of that method.

Subscripts

  • Subscript Syntax: subscripts enable you to query instances of a type by writing one or more values in square brackets after the instance name.
subscript(index: Int) -> Int {
    get {
        // Return an appropriate subscript value here.
    }
    set(newValue) {
        // Perform a suitable setting action here.
    }
}
  • Subscript Usage
  • Subscript Options: a class or structure can provide as many subscript implementations as it needs, and the appropriate subscript to be used will be inferred based on the types of the value or values that are contained within the subscript brackets at the point that the subscript is used. This definition of multiple subscripts is known as subscript overloading.
  • Type Subscripts

Inheritance

  • Defining a Base Class
    • Subclassing
    • Overriding
      • Accessing Superclass Methods, Properties and Subscripts
      • Overriding Methods
      • Overriding Properties
        • Overriding Property Getters and Setters
        • Overriding Property Observers
    • Preventing Overrides

Initialisation

  • Setting Initial Values for Stored Properties
    • Initialisers
    • Default Property Values
  • Customising Initialisation
    • Initialisation Parameters
    • Parameter Names and Argument Labels
    • Initialiser Parameters Without Argument Labels
    • Optional Property Types
    • Assigning Constant Properties During Initialisation
  • Default Initialisers
    • Memberwise Initialisers for Structure Types
  • Initialiser Delegation for Value Types
  • Class Inheritance and Initialisation: all of a class’s stored properties—including any properties the class inherits from its superclass—must be assigned an initial value during initialisation. Swift defines two kinds of initialisers for class types to help ensure all stored properties receive an initial value. These are known as designated initialisers and convenience initialisers.
    • Designated Initialisers and Convenience Initialisers. Designated initialisers are the primary initialisers for a class. Convenience initialisers are secondary, supporting initialisers for a class.
    • Syntax for Designated and Convenience Initialisers
    • Initialiser Delegation for Class Types: to simplify the relationships between designated and convenience initialisers, Swift applies the following three rules for delegation calls between initializers:

Rule 1

A designated initialiser must call a designated initialiser from its immediate superclass.

Rule 2

A convenience initialiser must call another initialiser from the same class.

Rule 3

A convenience initialiser must ultimately call a designated initialiser.

A simple way to remember this is: Designated initialisers must always delegate up. Convenience initialisers must always delegate across.

  • Two-Phase Initialisation: class initialisation in Swift is a two-phase process. In the first phase, each stored property is assigned an initial value by the class that introduced it. Once the initial state for every stored property has been determined, the second phase begins, and each class is given the opportunity to customise its stored properties further before the new instance is considered ready for use. Swift’s compiler performs four helpful safety-checks to make sure that two-phase initialisation is completed without error:
    1. Safety check 1
      A designated initialiser must ensure that all of the properties introduced by its class are initialised before it delegates up to a superclass initialiser.
      1. Safety check 2
        A designated initialiser must delegate up to a superclass initialiser before assigning a value to an inherited property. If it doesn’t, the new value the designated initialiser assigns will be overwritten by the superclass as part of its own initialisation.
      2. Safety check 3
        A convenience initialiser must delegate to another initialiser before assigning a value to any property (including properties defined by the same class). If it doesn’t, the new value the convenience initialiser assigns will be overwritten by its own class’s designated initialiser.
      3. Safety check 4
        An initialiser cannot call any instance methods, read the values of any instance properties, or refer to self as a value until after the first phase of initialisation is complete.

Day 115

Here’s how two-phase initialisation plays out, based on the four safety checks above:

Phase 1

  • A designated or convenience initialiser is called on a class.
  • Memory for a new instance of that class is allocated. The memory is not yet initialised.
  • A designated initialiser for that class confirms that all stored properties introduced by that class have a value. The memory for these stored properties is now initialised.
  • The designated initialiser hands off to a superclass initialiser to perform the same task for its own stored properties.
  • This continues up the class inheritance chain until the top of the chain is reached.
  • Once the top of the chain is reached, and the final class in the chain has ensured that all of its stored properties have a value, the instance’s memory is considered to be fully initialised, and phase 1 is complete.

Phase 2

  • Working back down from the top of the chain, each designated initialiser in the chain has the option to customise the instance further. Initialisers are now able to access self and can modify its properties, call its instance methods, and so on.
  • Finally, any convenience initialisers in the chain have the option to customise the instance and to work with self.

  • Initialiser Inheritance and Overriding: Swift subclasses do not inherit their superclass initialisers by default.
    • Automatic Initialiser Inheritance: assuming that we provide default values for any new properties we introduce in a subclass, the following two rules apply:
      Rule 1
      If our subclass doesn’t define any designated initialisers, it automatically inherits all of its superclass designated initialisers.
      Rule 2
      If our subclass provides an implementation of all of its superclass designated initialisers—either by inheriting them as per rule 1, or by providing a custom implementation as part of its definition—then it automatically inherits all of the superclass convenience initialisers.
      These rules apply even if your subclass adds further convenience initialisers.
    • Designated and Convenience Initialisers in Action
  • Failable Initialisers: we write a failable initialiser by placing a question mark after the init keyword (init?). NOTE: we cannot define a failable and a nonfailable initialiser with the same parameter types and names. Failable initialisers are implemented for numeric type conversions. To ensure conversion between numeric types maintains the value exactly, use the init(exactly:) initialiser. If the type conversion cannot maintain the value, the initialiser fails.
    • Failable Initialisers for Enumerations
    • Failable Initialisers for Enumerations with Raw Values
    • Propagation of Initialisation Failure
    • Overriding a Failable Initialiser
    • The init! Failable Initialiser
  • Required Initialisers: write the required modifier before the definition of a class initialiser to indicate that every subclass of the class must implement that initialiser. We must also write the required modifier before every subclass implementation of a required initialiser, to indicate that the initialiser requirement applies to further subclasses in the chain.
  • Setting a Default Property Value with a Closure or Function: if a stored property’s default value requires some customisation or setup, you can use a closure or global function to provide a customised default value for that property. Here’s a skeleton outline of how a closure can be used to provide a default property value:
    An example of this is the creation of a chessboard structure:

Deinitialisation

A deinitialiser is called immediately before a class instance is deallocated. We write deinitialisers with the deinit keyword, similar to how initialisers are written with the init keyword. Deinitialisers are only available on class types.

  • How Deinitialisation Works: class definitions can have at most one deinitialiser per class. The deinitialiser does not take any parameters and is written without parentheses.
  • Deinitialisers in Action

Optional Chaining

Optional chaining is a process for querying and calling properties, methods, and subscripts on an optional that might currently be nil.

  • Optional chaining as an Alternative to Forced Unwrapping
  • Defining Model Classes for Optional Chaining
  • Accessing Properties Through Optional Chaining
  • Calling Methods Through Optional Chaining
  • Accessing Subscripts Through Optional Chaining
    • Accessing Subscripts of Optional Type: if a subscript returns a value of optional type—such as the key subscript of Swift’s Dictionary type—place a question mark after the subscript’s closing bracket to chain on its optional return value.

Day 116

  • Linking Multiple Levels of Chaining
  • Chaining on Methods with Optional Return Values

Error Handling

  • Representing and Throwing Errors: throwing an error lets you indicate that something unexpected happened and the normal flow of execution can’t continue. You use a throw statement to throw an error.
  • Handling Errors: when a function throws an error, it changes the flow of our program, so it’s important that we can quickly identify places in our code that can throw errors. To identify these places in our code, we write the try keyword—or the try? or try! variation—before a piece of code that calls a function, method, or initialiser that can throw an error.
    • Propagating Errors Using Throwing Functions
    • Handling Errors Using Do-Catch: here is the general form of a do-catch statement:
    • Converting Errors to Optional Values
    • Disabling Error Propagation
  • Specifying Cleanup Actions: we use a defer statement to execute a set of statements just before code execution leaves the current block of code. Deferred actions are executed in the reverse of the order that they’re written in our source code.

Type Casting

Type casting is a way to check the type of an instance, or to treat that instance as a different superclass or subclass from somewhere else in its own class hierarchy. Type casting in Swift is implemented with the is and as operators.

  • Defining a Class Hierarchy for Type Casting
  • Checking Type: use the type check operator (is) to check whether an instance is of a certain subclass type. The type check operator returns true if the instance is of that subclass type and false if it is not.
  • Downcasting: a constant or variable of a certain class type may actually refer to an instance of a subclass behind the scenes. Where we believe this is the case, we can try to downcast to the subclass type with a type cast operator (as? or as!).
  • Type Casting for Any and AnyObject: Swift provides two special types for working with nonspecific types, Any can represent an instance of any type at all, including function types, while AnyObject can represent an instance of any class type.

Nested Types

  • Nested Types in Action
  • Referring to Nested Types

Extensions

Extensions add new functionality to an existing class, structure, enumeration, or protocol type. This includes the ability to extend types for which we do not have access to the original source code (known as retroactive modeling).

Extensions in Swift can:

  • Add computed instance properties and computed type properties
  • Define instance methods and type methods
  • Provide new initialisers
  • Define subscripts
  • Define and use new nested types
  • Make an existing type conform to a protocol

NOTE

Extensions can add new functionality to a type, but they cannot override existing functionality.


  • Extension Syntax: declare extensions with the extension keyword.
  • Computed Properties: extensions can add new computed properties, but they cannot add stored properties, or add property observers to existing properties.
  • Initialisers: extensions can add new convenience initialisers to a class, but they cannot add new designated initialisers or deinitialisers to a class.
  • Methods
    • Mutating Instance Methods
  • Subscripts
  • Nested Types

Protocols

A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements. Any type that satisfies the requirements of a protocol is said to conform to that protocol.

  • Protocol Syntax
  • Property Requirements: property requirements are always declared as variable properties, prefixed with the var keyword. Gettable and settable properties are indicated by writing { get set } after their type declaration, and gettable properties are indicated by writing { get }. We always need to prefix type property requirements with the static keyword when we define them in a protocol.
  • Method Requirements: protocols can require specific instance methods and type methods to be implemented by conforming types. These methods are written as part of the protocol’s definition in exactly the same way as for normal instance and type methods, but without curly braces or a method body. Example protocol:
    Example conforming class:
    Not that I understood what this does but …
  • Mutating Method Requirements
  • Initialiser Requirements
    • Class Implementations of Protocol Initialiser Requirements: we can implement a protocol initialiser requirement on a conforming class as either a designated initialiser or a convenience initialiser. In both cases, we must mark the initialiser implementation with the required modifier. If a subclass overrides a designated initialiser from a superclass, and also implements a matching initialiser requirement from a protocol, mark the initialiser implementation with both the required and override modifiers.
    • Failable Initialiser Requirements: a failable initialiser requirement can be satisfied by a failable or nonfailable initialiser on a conforming type. A nonfailable initialiser requirement can be satisfied by a nonfailable initialiser or an implicitly unwrapped failable initialiser.
  • Protocols as Types: protocols don’t actually implement any functionality themselves. Nonetheless, we can use protocols as a fully fledged types in our code. Using a protocol as a type is sometimes called an existential type, which comes from the phrase “there exists a type T such that T conforms to the protocol”. We can use a protocol in many places where other types are allowed, including:
    • As a parameter type or return type in a function, method, or initializer
    • As the type of a constant, variable, or property
    • As the type of items in an array, dictionary, or other container
  • Delegation: delegation is a design pattern that enables a class or structure to hand off (or delegate) some of its responsibilities to an instance of another type. This design pattern is implemented by defining a protocol that encapsulates the delegated responsibilities, such that a conforming type (known as a delegate) is guaranteed to provide the functionality that has been delegated. Delegation can be used to respond to a particular action, or to retrieve data from an external source without needing to know the underlying type of that source. To prevent strong reference cycles, delegates are declared as weak references. Here’s an example of delegation:
    Here are the protocols:
    …and here is the game example:

Day 117

Starting this day still in this article just to get finished with protocols. A very murky land! 🙂

  • Adding Protocol Conformance with an Extension
    • Conditionally Conforming to a Protocol
    • Declaring Protocol Adoption with an Extension
  • Collections of Protocol Types
  • Protocol Inheritance
  • Class-Only Protocols: we can limit protocol adoption to class types (and not structures or enumerations) by adding the AnyObject protocol to a protocol’s inheritance list.
  • Protocol Composition
  • Checking for Protocol Conformance
  • Optional Protocol Requirements
  • Protocol Extensions
    • Providing Default Implementations
    • Adding Constraints to Protocol Extensions

If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a small donation to support my studies and my writing (please appreciate that I am not using advertisement on my articles).

If you are interested in my music engraving and my publications don’t forget visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).

You can also support me by buying Paul Hudson’s books from this Affiliate Link.

Anyways, thank you so much for reading!

Till the next one!

Published by Michele Galvagno

Professional Musical Scores Designer and Engraver Graduated Classical Musician (cello) and Teacher Tech Enthusiast and Apprentice iOS / macOS Developer Grafico di Partiture Musicali Professionista Musicista classico diplomato (violoncello) ed insegnante Appassionato di tecnologia ed apprendista Sviluppatore iOS / macOS

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: