Code Like A Girl

Welcome to Code Like A Girl, a space that celebrates redefining society's perceptions of women in…

Follow publication

Introduction To Swift Programming (Part 6): Swift Cheat Sheet

Hina Khan
Code Like A Girl
Published in
9 min readNov 15, 2023

Image by Author on Canva

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 programmingseries, 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 tomatoesas 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!

Liked this article?
Give claps and show your support.

Connect with me on social media for more tips, tutorials, and updates:

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Published in Code Like A Girl

Welcome to Code Like A Girl, a space that celebrates redefining society's perceptions of women in technology. Share your story with us!

Written by Hina Khan

Software Engineer— Mobile App | Flutter | SwiftUI | AI | Machine Learning

Responses (1)

Write a response

Congratulations 👏🏽

--