Introduction To Swift Programming (Part 6): Swift Cheat Sheet
A comprehensive Guide on Classes, Protocols, Extensions, Enums, Initializers, and Deinitializers

Introduction:
In this article, we will quickly go through some of the advanced topics in Swift. The goal of this article is to equip you with a foundational understanding of these concepts so that when you encounter them in your Swift projects, you can know what they’re called and be able to look them up further.
This guide will provide a reference to a variety of advanced topics:
1. Classes, Inheritance, and Method Overriding
2. Extension
3. Protocols
4. Enumerations
5. Initializers and Deinitializers
Note: This article is a part of the “Introduction to Swift programming” series, ensuring a cohesive and structured learning experience.
Let's Get Started…..
1. Classes, Inheritance, and Method Overriding:
Swift allows us to create classes, and we can take advantage of inheritance and method overriding to build upon existing classes. Thus we can extend their functionality, and customize their behaviour to suit our application needs.
For example, we have a class like Number
that represents numeric values.
class Number {
var value: Int
init(value: Int) {
self.value = value
}
}
In the above code, we have a simple Number
class with a property value
and an initializer.
Inheritance:
Now, let’s explore the concept of inheritance, a fundamental object-oriented programming feature:
Inheritance enables us to create a new class, known as a subclass, that inherits properties and methods from a parent class, known as a superclass. Thus allowing us to extend and modify its behaviour.
For example, if we want to extend the functionality of an existing swift class like NSNumber
, we can subclass it and override its methods.
class SuperNumber: NSNumber {
override func getValue(value: UnsafeMutablePointer<Void> ) -> Int {
super.getValue(value)/
}
}
In this example, we’ve created a SuperNumber
class that is a subclass of NSNumber
. SuperNumber
inherits the value
property and the initializer from its superclass.
Method overriding:
Method overriding comes into play when we want to provide a custom implementation of a method in a subclass, replacing the behaviour inherited from the superclass.
Utilize the override
keyword and call the superclass's implementation within the overridden method using super
. For example:
class SuperNumber: NSNumber {
override func getValue() -> Int {
// Custom implementation for the getValue method
return super.getValue() * 2
}
}
In this code, we’ve overridden the getValue
method in the SuperNumber
class to return double the value of the original method.
2. Extensions:
Now, let’s dive into the concept of extensions and how they enhance classes without subclassing.
Why use Extensions?
Sometimes, we may want to add functionality to an existing class without creating a new subclass. Extensions provide an elegant solution.
Extensions are a Swift feature that enables us to add new functionality to existing classes without subclassing. So we can add new methods, computed properties, and initializers to an existing class, struct, enum, or protocol.
This can be particularly useful when we want to enhance a class like NSNumber
without changing its core definition.
Creating an Extension:
Suppose we want to add a new method, superCoolGetter()
, to the NSNumber
extension NSNumber {
func superCoolGetter() -> Int {
return 5
}
}
In the code snippet above, we’ve extended the NSNumber
class with a new method, superCoolGetter()
, that returns the integer 5. With this extension, we can use superCoolGetter()
on any NSNumber
instance.
Using Extensions:
Extensions make the new functionality available to all instances of the extended class.
We can now use the superCoolGetter()
method on any NSNumber
instance, even though NSNumber
itself doesn't include this method in its original definition.
let n = NSNumber(value: 42)
let result = n.superCoolGetter() // result is 5
In this example, we create an NSNumber
instance n
and call the superCoolGetter()
method, which returns 5.
So, it’s important to understand these two key concepts in Swift:
Subclassing: Subclassing involves creating a new class based on an existing class. This allows us to make slight alterations or customizations while inheriting the properties and methods of the original class.
Extending Classes: Extensions let us add new functions, methods, or variables to an existing class without modifying its source code. This is incredibly useful for enhancing class functionality.
3. Protocols:
Protocols serve as blueprints for defining a set of methods and properties that a class must adopt.
Think of them as interfaces in other programming languages.
Creating a protocol
Imagine a scenario where we need to work with various objects, all of which share a common trait — they can dance. To capture this commonality, we can create a protocol, which serves as an interface defining a blueprint for objects that can dance.
For example, we can create a protocol called Danceable
to define a method dance
.
protocol Danceable {
func dance()
}
Protocols are like contracts, specifying what a conforming type must implement.
Conforming to a Protocol:
Any class that conforms to this protocol must implement the dance
method. For example:
class Person: Danceable {
func dance() {
// Implement dance behavior here
}
}
Here, Person
adopts the Danceable
protocol by implementing the dance()
method. Omitting this implementation would trigger an error, enforcing adherence to the protocol.
Protocol Conformance and Inheritance:
Swift allows classes to conform to protocols and inherit from another class simultaneously.
In other words, a class can inherit from another class (even if it’s a system class like
NSNumber
) and still conform to one or more protocols.
For example:
class DancingNumber: NSNumber, Danceable {
func dance() {
// DancingNumber's dance implementation
}
}
Here, DancingNumber
is both an NSNumber
and conforms to the Danceable
protocol, demonstrating the flexibility that Swift offers.
We can also conform to multiple protocols and even extend existing swift classes to make them adopt protocols.
For example:
extension Person: Danceable, Singable {
func dance() {
// Implement dance behavior for NSNumber
}
func sing(){
// Implement sing behavior for NSNumber
}
}
In the above code, we extended a Person Class instead of subclassing it and also conform to multiple protocols like Singable and Danceable.
Why are protocols so valuable?
They bring order, structure, and flexibility to our code. Think of them as a way to separate concerns and ensure that various objects share specific behaviours without rigid inheritance hierarchies.
Imagine creating a class DanceFloor
that welcomes dancers. Instead of requiring all dancers to inherit from a common superclass (which can lead to messy object graphs), we can simply expect Danceable
entities. Whether it's a Person
, Cat
, or Dog
, if it conforms to Danceable
, it's welcome on the dance floor.
class DanceFloor {
func welcomeDancers(dancers: [Danceable]) {
for dancer in dancers {
dancer.dance()
}
}
}
The compiler ensures that anything we pass to welcomeDancers
adheres to the Danceable
protocol, keeping our code neat and ensuring that danceable entities indeed dance.
4. Enums:
Enums, short for enumerations, are a fundamental data type in Swift. Enum defines a type with a set of cases. Each case can represent a unique value or state.
Creating Enums:
Consider a scenario where we need to represent different types of vegetables. We can create an enum called TypesOfVeggies
with two cases: carrots
and tomatoes
as follows:
enum TypesOfVeggies {
case carrots
case tomatoes
}
Swift takes enums to the next level by allowing us to associate values with each case.
enum TypesOfVeggies: String {
case carrots = "Carrots"
case tomatoes = "Tomatoes"
case celery = "Celery"
}
In this example, each case is associated with a string value, providing meaningful names for each vegetable type.
Accessing Enum Values:
To access enum values, we can use dot notation.
let carrot = TypesOfVeggies.carrots
We can also access the raw value associated with an enum case:
print(carrot.rawValue) // Output: "Carrots"
Why Use Enums?
Enums add safety to the code. It allows us to define a limited set of valid options, which can prevent common programming errors.
Imagine a function called eatVeggies
that accepts a string parameter representing a vegetable type. Without enums, we risk accepting invalid inputs, like "lead" instead of a valid vegetable. However, by defining an enum, we ensure that only valid vegetable types are accepted.
func eatVeggies(veggie: TypesOfVeggies) {
switch veggie {
case .carrots:
print("Eating carrots!")
case .tomatoes:
print("Eating tomatoes!")
}
}
By accepting veggie
as an argument of type TypesOfVeggies
, we’re safe from passing incorrect values. We can only pass carrots
, tomatoes
, or celery
—a simple but effective way to prevent errors.
5. Initializers and Deinitializers
In Swift, initializers are fundamental for creating instances of classes, structures, and enums. They are responsible for setting up the initial state of an object.
Consider a class, Car
, which represents a car with cup holders.
When creating an instance of this class, it's essential to ensure that all its properties, including the cup holder, are properly initialized. Swift requires us to specify an initializer for this purpose.
Default Initializers:
The simplest way to provide an initializer is by creating a default initializer. For instance:
class Car {
var cupHolder: String = "DefaultCupHolder"
init() {
// our custom initialization logic can go here
}
}
let car = Car()
With the default initializer in place, we can create an instance of Car
without explicitly passing a cupHolder
value.
Custom Initializers:
Alternatively, we may want to define our custom initializers to provide more flexibility when creating instances. For instance:
class Car {
var cupHolder: String
init(cupHolder: String) {
// custom initialization logic
self.cupHolder = cupHolder
}
}
let car = Car(cupHolder: "CoolCupHolder")
In the example above, we define a custom initializer that takes a cupHolder
parameter. We must provide value for cupHolder
when creating an instance of Car
.
Required Initializers:
In some cases, we might want to create a required initializer, which means that it’s the only way to initialize the object.
Any subclass must also implement this required initializer.
class Vehicle {
var color: String
required init(color: String) {
self.color = color
}
}
let car=Car(color:"White")
Convenience Initializers:
On the other hand, convenience initializers provide alternative ways to initialize objects, making them more convenient.
However, they must always call another initializer, either a required or another convenience initializer from the same class.
class Car {
var color: String
required init(color: String) {
self.color = color
}
convenience init() {
self.init(color: "DefaultColor") // Call the required initializer
}
}
let car=Car();
In the example above, the convenience init
acts as a shortcut to create a car with a default cup holder, but it relies on the required initializer to set up the cupHolder
property.
Convenience initializers are useful when we want to create instances with default values or simplify the initialization process.
Initializer Hierarchy in Swift
In Swift, when dealing with class hierarchies, it’s essential to understand the hierarchy of initializers. This ensures that our classes and subclasses are properly initialized and that initializers from superclass to subclass are called in the correct order.
Consider a scenario where Car
is a subclass of another class, let's say, Vehicle
. When we create an instance of Car
, it's crucial to ensure that not only is its own initialization handled correctly but also the initialization of its superclass, Vehicle
.
(A required initializer must be implemented by any subclass. If a superclass defines a required initializer, the subclass must also implement it. This ensures that the subclass correctly initializes both its own properties and those inherited from the superclass.)
Here’s how the initializer hierarchy works:
class Vehicle {
var color: String
required init(color: String) {
self.color = color
}
}
class Car: Vehicle {
var cupHolder: String
required init(color: String, cupHolder: String) {
self.cupHolder = cupHolder
super.init(color: color) // Call the superclass's required initializer
}
}
In this example, Car
defines a required initializer that calls the superclass's required initializer using super.init(color:)
.
Deinitializers:
In Swift, we can also define deinitializers using the deinit
keyword.
A deinitializer is called automatically when an instance of a class is deallocated. It provides an opportunity to perform cleanup tasks or release any resources associated with the instance.
class Vehicle {
var color: String
required init(color: String) {
self.color = color
}
deinit {
// Cleanup tasks, if needed
}
}
Deinitializers are essential for managing resources and ensuring your classes clean up properly when they are no longer in use.
Conclusion:
Congratulations! You’ve explored classes, inheritance, and method overriding, discovered the flexibility of extensions and protocols, and harnessed the unique capabilities of enums. Initializers and deinitializers have become your tools for crafting well-structured code.
This article, part of the “Introduction to Swift programming” series, is your guidebook to understanding and applying these concepts effectively. As you move forward, the world of Swift programming is yours to conquer.
Happy coding!